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 知识体系总览

下面这张图,是我梳理的数据类型选择决策流程。每次拿不准的时候,照着走一遍就行。

数据类型选择决策流程 数据类型选择决策流程 业务字段定义 数值型 字符串型 日期时间型 枚举/布尔型 整数 → TINYINT/INT/BIGINT 小数 → DECIMAL/DOUBLE 定长 → CHAR 变长 → VARCHAR/TEXT 日期 → DATE 时间 → DATETIME/TIMESTAMP 布尔 → TINYINT(1)/BOOLEAN 枚举 → ENUM/TINYINT+字典 核心原则:业务语义优先,存储效率次之,查询性能兜底 先问「这个字段在业务里是什么」,再问「存多少数据」,最后问「怎么查」

4.7 避坑指南:我曾经踩过的五个坑

最后,分享几个我亲身经历过的数据类型陷阱。你遇到了,直接绕开走。

  1. 用 VARCHAR 存 IP 地址

    IP 地址本质上是 4 个字节的整数。用 VARCHAR(15) 存,不仅浪费存储,而且无法做范围查询。我建议用 INT UNSIGNED 存,配合 INET_ATON() 和 INET_NTOA() 转换。

  2. 用 FLOAT 存经纬度

    经纬度需要精确到小数点后 6 位,FLOAT 的精度只有 7 位有效数字,存经纬度会丢失精度。用 DECIMAL(10,6) 或者数据库原生的地理空间类型。

  3. 电话号码用 INT 存

    手机号 11 位,INT 最大 21 亿,存不下。而且手机号不需要做算术运算。用 VARCHAR(20) 最稳妥。

  4. 状态字段用 VARCHAR 存中文

    比如 '待支付'、'已支付'、'已退款'。用中文存,编码占空间大,而且国际化时得改数据。用 TINYINT 存状态码,展示层再映射。

  5. 主键用 UUID 字符串

    UUID 是 36 个字符的字符串,做主键不仅占空间,而且随机插入会导致索引碎片严重。用自增 BIGINT 或者雪花算法生成的整数 ID。

最后提醒一句:数据类型选错了,后期改的成本是前期的 10 倍以上。建表前多花 10 分钟想清楚,后面能省 10 个小时的返工时间。别问我怎么知道的——我改过一张 50 亿行的表,改了三天三夜。