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 类型选择决策流程
说了这么多,到底怎么选?我画了一张决策图,你照着走一遍基本不会出错。
3.5 性能权衡实战建议
最后,我总结几条实战中验证过的建议:
- 能用整型绝不用字符串。比如手机号,虽然看起来是数字,但不会做加减运算,存成
VARCHAR(11)没问题。但如果是用户 ID,一定用INT或BIGINT。 - 日期字段别用字符串。这个我前面说过了,性能差距 10 倍以上。
- DECIMAL 精度够用就行。多一位小数,存储多 1-2 字节,1 亿行就是 200MB 的差距。
- 枚举值用 TINYINT。配合字典表,既节省空间又保持可读性。
- NULL 值要谨慎。有些数据库对 NULL 的索引支持不好,能用 NOT NULL DEFAULT 值就别用 NULL。
我的习惯:每次设计宽表前,先跑一遍数据探查,看看每个字段的实际值范围、长度分布、NULL 比例。有了这些数据,选型就不会拍脑袋。说白了,字段类型选对了,宽表性能就成功了一半。