3. 字段类型选择:数值型、字符型、日期型的选择策略、精度与性能的权衡

字段类型选不对,宽表跑起来就像穿了不合脚的鞋——看着能走,实际上每一步都难受。我见过太多因为类型选错导致的性能灾难,今天咱们就把数值、字符、日期这三类字段的选型逻辑掰开揉碎讲清楚。

核心原则:能用窄类型不用宽类型,能用定长不用变长,能用整型不用字符串。这不是教条,是拿真金白银换来的教训。

3.1 数值型字段:精度与存储的博弈

数值型字段的坑,我踩过最深的那个跟精度有关。有一次做金融宽表,交易金额用了 FLOAT,结果对账时差了 0.01 元。排查了一整天,最后发现是浮点数精度丢失。从那以后,涉及钱的字段我全用 DECIMAL。

3.1.1 整型 vs 小数型

类型 存储大小 适用场景 我的建议
TINYINT 1 字节 状态码、枚举值(0-255) 性别、订单状态首选
SMALLINT 2 字节 年龄、月份、小范围计数 够用就别用 INT
INT 4 字节 用户 ID、订单号、时间戳 宽表中最常见的整型
BIGINT 8 字节 超大规模 ID、雪花 ID 别滥用,存储翻倍
DECIMAL(p,s) 可变 金额、税率、精确计算 金融场景必须用
FLOAT/DOUBLE 4/8 字节 科学计算、近似值 宽表里尽量别碰

我的习惯:状态字段用 TINYINT 而不是 VARCHAR。比如订单状态 0=待支付、1=已支付、2=已发货,用数字比用字符串快 30% 以上,而且存储省了 90%。

3.1.2 DECIMAL 精度陷阱

DECIMAL 的精度定义直接影响存储和性能。我见过有人把金额定义成 DECIMAL(38,10),结果一张 1 亿行的宽表光这个字段就占了 20GB。其实大部分场景 DECIMAL(18,4) 就足够了。

-- 推荐:金额字段用 DECIMAL(18,4)
CREATE TABLE fact_order (
    order_id INT,
    amount DECIMAL(18,4),  -- 18位总长,4位小数
    tax DECIMAL(10,4)      -- 税率精度可以更高
);

-- 不推荐:过度定义精度
-- amount DECIMAL(38,10)  -- 浪费存储,查询更慢

避坑指南:我曾经把一个用户积分字段设成 DECIMAL(10,2),结果积分永远是整数,白白浪费了 2 位小数位。后来改成 INT,存储减半,查询更快。记住:小数位不是摆设,用不上就别定义。

3.2 字符型字段:定长与变长的抉择

字符型字段是宽表里最容易出问题的类型。你想想看,一个 VARCHAR(1000) 的字段,如果实际只存了 10 个字符,在 MySQL 里还好,但在列式存储的 ClickHouse 里,变长字段的压缩率会大打折扣。

3.2.1 CHAR vs VARCHAR

特性 CHAR VARCHAR
存储方式 定长,不足补空格 变长,加 1-2 字节长度前缀
性能 随机读写更快 空间利用率高
适用场景 固定长度编码(身份证号、手机号) 长度不固定的文本(名称、地址)
宽表推荐 尽量少用,除非长度固定 首选,但要控制最大长度

3.2.2 字符串长度控制

我见过最离谱的宽表,有个 VARCHAR(5000) 的备注字段,实际平均长度只有 50。这种设计不仅浪费存储,还拖慢全表扫描。我的经验是:先抽样分析数据,再定长度。

-- 先分析实际数据长度
SELECT 
    MAX(LENGTH(remark)) AS max_len,
    AVG(LENGTH(remark)) AS avg_len,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY LENGTH(remark)) AS p95_len
FROM source_table;

-- 根据结果定义字段长度
-- 如果 p95_len = 120,定义 VARCHAR(200) 就够用了

我的做法:对于枚举值字段,比如国家代码、货币类型,我习惯用 CHAR(2) 或 CHAR(3)。定长字段在排序和分组时性能更好,而且存储可预测。

3.3 日期型字段:时间精度与存储的平衡

日期字段选型,说白了就是回答一个问题:你需要精确到秒还是毫秒?我见过有人把时间戳存成 VARCHAR,然后每次查询都要做类型转换——这种设计,性能能好才怪。

3.3.1 日期类型对比

类型 存储大小 精度 适用场景
DATE 3 字节 天 生日、注册日期、统计日期
DATETIME 5-8 字节 秒/毫秒 订单时间、操作日志
TIMESTAMP 4 字节 秒 记录创建/更新时间
INT/BIGINT 4/8 字节 秒/毫秒 跨数据库兼容、时间分区

3.3.2 时间戳选型策略

我个人习惯:如果宽表只做日级别聚合,用 DATE 就够了。需要精确到秒的场景,用 TIMESTAMP 或 INT 存储 Unix 时间戳。为什么?因为 TIMESTAMP 只有 4 字节,比 DATETIME 省空间,而且时区自动转换。

-- 场景1:日统计宽表
CREATE TABLE dws_daily_stats (
    stat_date DATE,          -- 3字节,够用
    user_id INT,
    order_count INT
);

-- 场景2:实时宽表
CREATE TABLE dws_realtime (
    event_time TIMESTAMP,    -- 4字节,精确到秒
    -- 或者用 INT 存 Unix 时间戳
    event_ts INT,            -- 4字节,跨数据库兼容
    user_id INT,
    event_type TINYINT
);

避坑指南:我曾经把时间字段存成 VARCHAR(19),格式是 'YYYY-MM-DD HH:MM:SS'。结果每次按时间范围过滤都要做 STR_TO_DATE 转换,一个 5000 万行的表,查询慢了 10 倍。后来改成 TIMESTAMP,同样的查询从 30 秒降到 2 秒。记住:数据库原生日期类型比字符串快一个数量级。

3.4 类型选择决策流程

说了这么多,到底怎么选?我画了一张决策图,你照着走一遍基本不会出错。

字段类型选择决策流程 字段类型选择 数据是什么类型? 数值 字符 日期 需要精确小数? 是 → DECIMAL(p,s) 否 → 整型 长度固定? 是 → CHAR(n) 否 → VARCHAR(n) 精度到天? 是 → DATE 否 → TIMESTAMP 最终确定字段类型 💡 决策要点: • 能用窄类型不用宽类型(TINYINT > INT > BIGINT) • 能用定长不用变长(CHAR > VARCHAR,但仅限长度固定场景)

3.5 性能权衡实战建议

最后,我总结几条实战中验证过的建议:

  1. 能用整型绝不用字符串。比如手机号,虽然看起来是数字,但不会做加减运算,存成 VARCHAR(11) 没问题。但如果是用户 ID,一定用 INT 或 BIGINT。
  2. 日期字段别用字符串。这个我前面说过了,性能差距 10 倍以上。
  3. DECIMAL 精度够用就行。多一位小数,存储多 1-2 字节,1 亿行就是 200MB 的差距。
  4. 枚举值用 TINYINT。配合字典表,既节省空间又保持可读性。
  5. NULL 值要谨慎。有些数据库对 NULL 的索引支持不好,能用 NOT NULL DEFAULT 值就别用 NULL。

我的习惯:每次设计宽表前,先跑一遍数据探查,看看每个字段的实际值范围、长度分布、NULL 比例。有了这些数据,选型就不会拍脑袋。说白了,字段类型选对了,宽表性能就成功了一半。