4、数据类型选择策略:如何根据业务场景选择合适的数据类型,避免常见陷阱
数据类型选错了,后面全得改。这话听起来有点,但我在数据治理项目里见过太多次了——就因为当初建表时随手选了个 VARCHAR(255),结果跑数仓时转换报错、存储膨胀、查询慢得像蜗牛。说白了,数据类型选不对,Schema 治理的第一步就歪了。
这一章,我把自己踩过的坑和总结出来的策略掰开揉碎讲给你听。你想想看,一个字段选什么类型,真的只是「能存下数据就行」吗?
4.1 数据类型选择的四个维度
我个人习惯把数据类型选择拆成四个维度来评估。每次建表前,拿这四个问题过一遍,基本不会出大错。
| 维度 | 核心问题 | 常见陷阱 |
|---|---|---|
| 业务语义 | 这个字段在业务里到底是什么? | 用字符串存日期、用浮点数存金额 |
| 存储效率 | 数据量级多大?需要多少字节? | 一律用 BIGINT 或 TEXT |
| 查询性能 | 这个字段会被频繁过滤或排序吗? | 字符串字段上建索引导致性能灾难 |
| 可扩展性 | 未来数据范围会变吗? | 枚举类型不够用,改表结构成本高 |
举个例子。我在一个电商项目中,看到订单表的「支付金额」字段用的是 DECIMAL(10,2)。乍一看没问题,但业务上线半年后,单笔订单金额超过了 1 亿(B2B 大额采购),直接溢出报错。嗯,这里要注意——DECIMAL 的精度和标度一定要留够余量。
4.2 数值类型:别小看那几位小数
数值类型的选择,说白了就是搞清楚三件事:整数还是小数?范围多大?精度要求多高?
4.2.1 整数类型
| 类型 | 存储 | 范围 | 适用场景 |
|---|---|---|---|
TINYINT |
1 字节 | -128 ~ 127 | 状态码、性别、开关标识 |
SMALLINT |
2 字节 | -32768 ~ 32767 | 年龄、评分、小范围枚举 |
INT |
4 字节 | -2^31 ~ 2^31-1 | 用户 ID、订单号(大部分场景) |
BIGINT |
8 字节 | -2^63 ~ 2^63-1 | 雪花 ID、流水号、超大规模数据 |
TINYINT 绝不用 INT。一张表几亿行,每个字段省 3 个字节,整体存储能降 30% 以上。我曾经优化过一张 5 亿行的日志表,把状态字段从 INT 改成 TINYINT,存储从 120GB 降到了 85GB。
4.2.2 小数类型
这里有个经典陷阱:用 FLOAT 或 DOUBLE 存金额。为什么不行?因为浮点数是近似值,0.1 + 0.2 可能等于 0.30000000000000004。你想想看,财务对账差一分钱,查起来多痛苦。
我的建议:
- 金额、税率、单价 → 必须用
DECIMAL,精度和标度要明确。比如DECIMAL(18,4)存原始金额,展示时再四舍五入。 - 科学计算、百分比、坐标 → 可以用
DOUBLE,但要接受精度损失。 - 千万别用
FLOAT——精度太差,而且不同数据库实现有差异,迁移时容易出问题。
4.3 字符串类型:VARCHAR 不是万能药
我见过最夸张的一张表,所有字段全是 VARCHAR(255),包括日期、数字、甚至布尔值。这种设计,说白了就是偷懒。后果是什么?存储膨胀、索引失效、查询慢、数据质量一塌糊涂。
4.3.1 CHAR vs VARCHAR
| 类型 | 特点 | 适用场景 |
|---|---|---|
CHAR(N) |
固定长度,不足补空格 | 固定长度的编码、MD5、UUID |
VARCHAR(N) |
可变长度,按实际存储 | 大部分文本字段 |
VARCHAR(255) 和 VARCHAR(256) 在 MySQL 中存储机制不同——255 以内用 1 字节记录长度,256 以上用 2 字节。所以,如果字段长度确定不超过 255,就写 VARCHAR(255),别随手写个 500。
4.3.2 TEXT 类型要慎用
TEXT 类型虽然能存大文本,但有很多限制:不能建全文索引(某些数据库除外)、排序慢、不能有默认值。我在项目中遇到过,有人用 TEXT 存用户备注,结果查询时全表扫描,一个简单的 COUNT 查询跑了 30 秒。
我的建议:
- 长度 < 255:用
VARCHAR - 长度 255 ~ 5000:用
VARCHAR或MEDIUMTEXT - 长度 > 5000:考虑单独建表,或者用对象存储 + 链接
4.4 日期时间类型:别用字符串存时间
这个坑我踩过不止一次。早期有个项目,开发图方便,把「创建时间」存成了 VARCHAR(19),格式是 'YYYY-MM-DD HH:MM:SS'。结果呢?
- 按月份分组统计时,得先做字符串截取,性能极差
- 时区转换全靠手动拼 SQL
- 排序是按字典序排的,'2023-01-01' 排在 '2023-11-01' 后面
后来我花了整整两天,写脚本把所有历史数据清洗了一遍,才改成 DATETIME 类型。嗯,从那以后,我再也不敢用字符串存时间了。
| 类型 | 存储 | 精度 | 适用场景 |
|---|---|---|---|
DATE |
3 字节 | 日 | 生日、账单日、只关心日期 |
TIME |
3 字节 | 秒 | 营业时间、耗时统计 |
DATETIME |
8 字节 | 秒 | 通用时间记录(推荐) |
TIMESTAMP |
4 字节 | 秒 | 跨时区场景、自动更新 |
DATE 而不是 DATETIME,每个字段省 5 个字节。一张 10 亿行的表,能省 50GB 存储。
4.5 枚举与布尔:简单场景的利器
布尔值用 TINYINT(1) 还是 BIT(1)?枚举用 ENUM 还是 VARCHAR?这些问题看似简单,但选错了也会埋坑。
4.5.1 布尔类型
- MySQL:没有原生 BOOLEAN,用
TINYINT(1),0 表示 false,1 表示 true - PostgreSQL:有原生
BOOLEAN,直接用 - 别用
BIT(1):虽然存储更小,但查询时容易出类型转换问题
4.5.2 枚举类型
ENUM 的好处是存储小、可读性好。但有个大问题:扩展性差。我曾经在一个项目中,订单状态用 ENUM('pending','paid','shipped'),后来业务加了 'cancelled' 和 'refunded',不得不执行 ALTER TABLE 修改枚举值。表里有 2 亿行数据,这个 ALTER 操作锁表了 40 分钟。
我的建议:
- 枚举值确定且不会变(比如性别、星期)→ 用
ENUM - 枚举值可能增加(比如订单状态、审批状态)→ 用
TINYINT+ 字典表,或者VARCHAR+ 代码表
4.6 知识体系总览
下面这张图,是我梳理的数据类型选择决策流程。每次拿不准的时候,照着走一遍就行。
4.7 避坑指南:我曾经踩过的五个坑
最后,分享几个我亲身经历过的数据类型陷阱。你遇到了,直接绕开走。
-
用 VARCHAR 存 IP 地址
IP 地址本质上是 4 个字节的整数。用
VARCHAR(15)存,不仅浪费存储,而且无法做范围查询。我建议用INT UNSIGNED存,配合INET_ATON()和INET_NTOA()转换。 -
用 FLOAT 存经纬度
经纬度需要精确到小数点后 6 位,
FLOAT的精度只有 7 位有效数字,存经纬度会丢失精度。用DECIMAL(10,6)或者数据库原生的地理空间类型。 -
电话号码用 INT 存
手机号 11 位,
INT最大 21 亿,存不下。而且手机号不需要做算术运算。用VARCHAR(20)最稳妥。 -
状态字段用 VARCHAR 存中文
比如 '待支付'、'已支付'、'已退款'。用中文存,编码占空间大,而且国际化时得改数据。用
TINYINT存状态码,展示层再映射。 -
主键用 UUID 字符串
UUID 是 36 个字符的字符串,做主键不仅占空间,而且随机插入会导致索引碎片严重。用自增
BIGINT或者雪花算法生成的整数 ID。