很多人把字段类型设计当作单纯的“这个字段存什么”的决定,但实际上,类型选错带来的性能代价远比想象中大。字段类型直接影响每行数据的存储空间、索引的大小和扫描速度、内存缓冲池的利用率,甚至决定一条 SQL 能不能走索引。设计时多花几分钟斟酌类型,往往比后期加索引、扩内存效果更持久。
字段类型为什么会影响性能
数据库的每次 I/O 都是以页(默认 16KB)为单位的。单行数据越紧凑,一个数据页里就能装下越多行。这意味着:
- 磁盘 I/O 减少:同样的查询,紧凑的表可能一次读几页就够了,松散的表可能要读几十页。
- 缓冲池利用率更高:内存里能缓存的有效行数更多,命中率自然更高。
- 索引更小更快:索引本身也要存键值,键值越小,每个索引页装的键就越多,B+ 树的高度更低、扫描更快。
反之,如果字段设计得过于“宽敞”,比如能用 INT 存的数据用了 BIGINT,能用 VARCHAR(100) 用了 VARCHAR(500),甚至直接使用 TEXT,那么不仅浪费磁盘,还会造成内存和 CPU 的全线浪费。在生产上,这种浪费会随着数据量放大,最终拖垮整体性能。
数值类型:够用、合适、有余地
数值类型是查询最频繁、最常作为索引键的类型,设计要点很直接:在能覆盖业务最大预期值的前提下,选择最小的类型。
| 类型 | 存储空间 | 有符号范围 | 适用场景 |
|------------|----------|--------------------------------|-------------------------------|
| TINYINT | 1 字节 | -128 ~ 127 | 状态标记、布尔值、小枚举 |
| SMALLINT | 2 字节 | -32768 ~ 32767 | 端口号、小计数值 |
| MEDIUMINT | 3 字节 | 约 ±830 万 | 较少使用,可用 INT 替代 |
| INT | 4 字节 | 约 ±21 亿 | 主键、数量、用户 ID |
| BIGINT | 8 字节 | 约 ±9.2×10¹⁸ | 超大 ID、高精度时间戳 |
| DECIMAL(M,N) | 变长 | 精确小数 | 金额等必须精确的场景 |
| FLOAT/DOUBLE | 4/8字节 | 近似值 | 科学计算、少量精度容忍场景 |
日常中最常见的误区就是主键使用 BIGINT 自增。如果预估用户量在千万到亿级,INT 完全够用,省下的 4 字节在主键索引和所有二级索引中都会产生连锁收益。一个 1 亿行的表,主键从 BIGINT 改为 INT,光主键索引就能节省约 380MB,加上二级索引轻松节省上 GB。
对于金额,强烈推荐使用 DECIMAL,并在程序中用字符串或专门的 Money 类型承接,绝不使用 FLOAT/DOUBLE。浮点数的精度问题可能会在多次运算后产生分位数误差,这在财务系统里是不可接受的。
另外注意:MySQL 8.0 支持 CHECK 约束,你可以配合数值类型加范围验证,比如 CHECK (age >= 0 AND age <= 200),在数据库层兜底。
字符串类型:能用 CHAR 就别用 VARCHAR,能用 VARCHAR 就别用 TEXT
字符串类型的选择直接决定了索引效率和内存消耗。
- CHAR(N):定长,N 表示字符数(不是字节数)。对于长度固定的值(如 MD5 值、身份证号、手机号),使用 CHAR 比 VARCHAR 更合适,因为免去了长度标记和变长处理的额外开销,查询速度更快。而且 CHAR 最大 255 字符,业务中谨慎使用。
- VARCHAR(N):变长,实际占用 N 个字符的长度加 1 或 2 字节长度前缀。VARCHAR 是字符串的默认选择,但 N 不要设得过大,比如
VARCHAR(500)而实际存的数据不超过 20 个字符,MySQL 优化器会按声明的最大长度估算内存消耗,可能导致生成错误的执行计划(比如放弃使用内存临时表而去创建磁盘临时表)。 - TEXT / BLOB:大对象,占用空间大,并且有独立的外部存储页。它们不能有默认值,前缀索引是优化它们的常规手段。尽量避免在查询中使用未建立前缀索引的 TEXT 列,否则会造成大量磁盘 I/O。
还有个容易暴雷的点:字符集和排序规则(Collation)。在 MySQL 8.0 里默认字符集是 utf8mb4,一个字符最多占 4 字节。如果你创建 CHAR(10) 并且字符集是 utf8mb4,那么这列至少占用 40 字节存储。设计字段长度时要心中有数,不要只是在 varchar 里拍脑门写个 255。
索引友好性也是字符串类型选型要考虑的。对于需要作为索引列的字符串,需要更严格地对待长度。对于太长的 VARCHAR,可以创建前缀索引(INDEX (col(10))),但前缀索引无法用于排序和覆盖索引,效果大打折扣。更好的办法可能是单独存一个哈希值或编号列来辅助索引。
日期时间类型:别再犹豫用哪种了
| 类型 | 存储空间 | 范围 | 时区影响 |
|-----------|----------|------------------------------|------------------|
| DATE | 3 字节 | 1000-01-01 ~ 9999-12-31 | 无 |
| TIME | 3 字节 | -838:59:59 ~ 838:59:59 | 无 |
| DATETIME | 5 字节(8.0中5+小数秒) | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 无 |
| TIMESTAMP | 4 字节+小数秒 | 1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC | 自动转换时区 |
两条铁律:
- 只要业务不要求“根据客户端时区自动显示时间”,就用 DATETIME。它没有 2038 年的溢出风险,且行为完全可控。
- 如果业务需要存储时区无关的时间点(比如日志记录),且时间范围在 1970-2038 之间,可以用 TIMESTAMP,它能自动将插入时的当前会话时区转为 UTC 存储,查询时再转回。
一个性能注意:TIMESTAMP 在 MySQL 5.6 之后对 NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 非常友好,但如果表中有多个 TIMESTAMP 列,要小心自动初始化和更新行为带来的逻辑不一致。
关于存储空间:DATETIME 在 MySQL 5.6.4 之后优化到 5 字节加上小数秒(0-3 字节),之前是 8 字节。所以现在空间差异很小,没必要因字节数牺牲功能。
是否使用 NULL:用 NOT NULL 加上合理的默认值
NULL 在数据库里并不是“没值”,它代表未知。设计字段时如果允许 NULL,会带来三个弊端:
- 索引额外开销:对于允许 NULL 的列,InnoDB 会使用额外的一个字节来标记该列是否为 NULL,每个行都会增加存储。
- 查询语义复杂:
WHERE col = NULL是无效的,必须写成IS NULL,容易被疏忽。 - 聚合函数忽略 NULL:
COUNT(col)不计 NULL 值,而COUNT(*)算所有行,这个差异经常导致隐晦的统计错误。
因此最佳实践是:所有列尽量声明为 NOT NULL,并给出合理的默认值。状态列默认 0,字符串默认空串,数字默认 0。如果真需要区分“未知”与“空”,再用约定的特殊值(比如 -1 或 'UNKNOWN'),这样索引利用率和查询清晰度明显提高。
总结:字段设计原则清单
- 最小化存储:在满足业务未来 3-5 年容量预期的前提下,选择最小的数据类型。
- 索引列尤其精打细算:主键尽量是紧凑的 INT 自增,避免用字符串做主键;二级索引的键值长度直接决定了索引大小。
- 字符串长度要真实:根据实际业务数据统计确定 VARCHAR 长度,不要随手设 255。
- 日期时间统一用 DATETIME,除非明确需要时区转换。
- 禁用 FLOAT/DOUBLE 存储金额,用 DECIMAL。
- 字段尽量 NOT NULL,提供默认值。
- 字符集用 utf8mb4,但要清楚它带来的存储放大效应。
这些原则不是教条,但在大多数业务场景中照做,能帮你避免后期数据库膨胀和大量慢查询问题。字段类型就是存储架构的“地基”,地基打窄了,楼盖不高。