数据库表设计的质量,一半在逻辑模型,另一半在字段类型选择。选对类型不仅能节省存储空间,还能减少隐式转换、加速索引查找、避免诡异的业务 bug。选错类型,轻则浪费磁盘和内存,重则查询不走索引、数据被截断或溢出,甚至在高并发时拖垮整条链路。
字段类型选型有一个很朴素的原则:用最小的、最贴近数据真实特征的、最适合查询方式的类型。
7.3.1 数值类型:精打细算每一字节
MySQL 提供了丰富的数值类型,分为整数、定点数、浮点数三类。选型时的核心考量是:数据范围、是否需要精确计算、是否会用于索引。
整数类型 是最常用的类型。从 TINYINT 到 BIGINT,存储字节依次为 1、2、3、4、8 字节。一个很常见的错误是全表主键一律用 BIGINT,哪怕这个表一辈子只有几万行。主键大了,二级索引的叶子节点全跟着变大,缓存命中率下降,I/O 量上升。所以:
- 状态、类型、布尔值等小范围枚举,用 TINYINT(1 字节)。MySQL 没有真正的 BOOLEAN 类型,
BOOL或BOOLEAN实际是 TINYINT(1) 的同义词。存储 0/1 足够。 - 常规业务数量、计数器、数量不大的 ID,用 INT(4 字节),范围 ±21 亿,足够应付绝大多数中型表。
- 只有明确知道需要超过 21 亿或有超大增量(如日志、轨迹、全局序列号)时,再用 BIGINT。不要因为“将来可能需要”而盲目选大类型——将来改类型有工具,但存储成本是你现在每天在支付的。
对于显示宽度(如 INT(11)),需要特别注意:这个括号里的数字在 8.0 里只对 ZEROFILL 有影响,跟存储大小毫无关系,更不是字段长度限制。你完全可以忽略它,或者统一不写。真正限制数值范围的是数据类型本身。
浮点数与定点数 的区别在于是否精确。FLOAT 和 DOUBLE 是近似值类型,遵循 IEEE 754,用于科学计算或不需要完全精确的场景(如阅读量、评分)。它们不能用在需要精确对账的场合——0.1 在浮点数里是个无限循环小数,累加迟早出偏差。涉及金额、账务、库存,必须用 DECIMAL(定点数),例如 DECIMAL(10,2) 表示最多 10 位数字,其中 2 位小数。它按字符串方式存储,不会产生浮点误差,代价是存储稍大且计算稍慢。
一个折中但不太常见的做法是把金额存为 BIGINT 表示“分”,这样完全用整数运算,精度零损耗且效率高,但也失去了字段直接表明含义的清晰性。看团队习惯,两种方式都可以。
7.3.2 字符串类型:长度与存储的博弈
字符串是字段选型中最容易出现性能陷阱的地方。核心是 CHACH 类型的选择、长度设计,以及字符集的影响。
CHAR 与 VARCHAR 的区别 不仅仅是定长和变长那么简单。
- CHAR(N):固定长度,N 是指字符数,范围 0~255。存储时总是占用 N×字符集编码字节数,不够的用空格填充(查询时会去除尾部空格)。因此如果数据长度基本固定,比如状态码、MD5 值、手机号、身份证号,用 CHAR 更合适,没有额外开销。
- VARCHAR(N):变长,N 也是字符数,最大 65535 字节范围(需减去长度标识等开销)。实际存储取决于插入数据的实际字符数和编码。适合长度波动大的数据,如用户名、邮箱、地址。但它有两个成本:额外 1~2 字节记录实际长度;更新时如果新值比原值长,可能导致原位置放不下,产生页内碎片甚至页拆分,影响性能。
一个极为重要的约束是 索引最大长度限制:InnoDB 默认索引前缀最大 767 字节(5.7 中默认,8.0 可配置到 3072)。如果给一个 VARCHAR(255) 的 utf8mb4 列建索引,255×4=1020 字节,会直接报错或者被截断为前缀索引。因此,为长字符串建索引,要么用前缀索引(如 KEY(column(20))),但前缀索引不能用于 ORDER BY 和 GROUP BY;要么考虑把较长的字符串(如长网址、长标题)拆出短字段索引。在很多场合,宁可新增一个定长的 URL_hash 列用于查询,也比玩命压缩索引更稳妥。
TEXT 与 BLOB:对于超过 4000 字符,或明确是“文章正文、评论内容、大段 JSON”这种很可能很大的数据,不要试图用 VARCHAR(10000) 硬塞。VARCHAR 最大 65535 字节,但这是一行的所有变长列的总和,且过长的 VARCHAR 会有各种隐性问题。应使用 TEXT(有字符集)或 BLOB(二进制)。但切记:TEXT/BLOB 可能会导致临时表走磁盘、排序不使用内存,查询时避免 SELECT * 拖出全量。大部分场景下,只存储大字段,查询时单独按需获取。
字符集的选择 强烈推荐 utf8mb4。MySQL 中的 “utf8” 其实是个阉割版,每个字符最多 3 字节,无法存储 emoji 和部分生僻汉字。只有 utf8mb4 是真正的 UTF-8。新项目直接全库、全表、全字段字符集统一为 utf8mb4,排序规则用 utf8mb4_unicode_ci 或 utf8mb4_general_ci(前者排序更标准,后者稍快)。不要在这一步贪图一点点存储节能用 utf8,后期改字符集重建表是一笔不小的债务。
7.3.3 日期时间类型:选对就有“时间线”意识
日期时间字段涉及比较、排序、时间计算,选错类型会让查询变得异常复杂。
- DATETIME:日期加时间,格式
YYYY-MM-DD HH:MM:SS,范围从 1000 到 9999 年,不受时区影响,你存进去什么就存什么。业务记录(创建时间、更新时间)最好都用它,清晰不变。 - TIMESTAMP:时间戳,范围仅 1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC(受 Unix 时间戳限制)。它的存储实际是 UTC 时间,在读写时根据会话时区自动转换。所以如果你的服务可能跨时区,TIMESTAMP 能自动转成本地时间显示,但如果迁移服务器、修改时区,读出来的值可能变掉,容易踩坑。
- DATE 和 TIME:需要单独日期或时间时使用,不用硬凑 DATETIME 然后截取。
- YEAR:8.0 已不推荐,建议直接用 SMALLINT 或 INT 存年份。
一个实用习惯是:所有需要人工阅读的时间都用 DATETIME,需要时区自动转换且不担心 2038 问题的场景用 TIMESTAMP。 不要用 VARCHAR 来存时间——“2025-03-01”这样存,不仅占用大,索引无法用内置时间函数优化,排序还变成字符串字典序而非时间顺序,坑深且无谓。
7.3.4 特殊类型与选型陷阱
MySQL 还提供了 ENUM、SET、JSON 等特殊类型,各有优劣势。
- ENUM:枚举字符串,内部用整数存储,比较紧凑。但后期增加或修改枚举值需 ALTER TABLE,在数据量大时可能很慢。而且它的排序是按内部整数顺序,而非字符串。通常更推荐用 TINYINT 或 CHAR 加检查约束或代码校验,来避免 ENUM 的运维麻烦。
- SET:多选多,存储效率可以,但同样修改成本高,一般业务中很少依赖数据库做 SET 运算,更多交给应用层。
- JSON:MySQL 8.0 对 JSON 的支持增强很多,可以建立虚拟列并对其字段建立索引。将非结构化或半结构化数据存为 JSON 能带来很大灵活性,但要注意:JSON 文本不宜频繁修改,否则产生碎片;查询通常需要函数或 JSON_EXTRACT,不如原生列效率高。适合存配置、扩展信息,不适合做核心业务的频繁过滤条件。
NULL 与 NOT NULL 也是一个选型时必须考虑的点。虽然 NULL 是“缺失值”的标准表示,但索引对 NULL 值的处理有所不同,某些比较可能出意外。如果字段有明确默认值且业务上允许未知,建议给默认值并设为 NOT NULL,比如数值 0、空字符串 ''(注意空串和 NULL 是两回事)。尤其是需要排序、分组的字段,NULL 值往往导致结果不符合预期。
7.3.5 选型检查清单
设计表或审核建表语句时,可以逐项自问:
- 这个 ID 的最大值可能超过 20 亿吗? 一般用 INT,很大才用 BIGINT。
- 这个小数需要精准计算吗? 钱用 DECIMAL 或 BIGINT(分)。
- 字符串长度是固定还是变化剧烈? 固定用 CHAR,不定用 VARCHAR,避免过度放大长度。
- 会在这个长字符串上建索引吗? 考虑前缀索引或哈希辅助列。
- 字符集是 utf8mb4 吗? 无特殊理由不要用 utf8。
- 时间是否跨时区? 一般 DATETIME,需要时区转换时用 TIMESTAMP。
- 有没有乱用 ENUM/SET? 优先考虑小整数加字典表。
- 所有字段都有 NOT NULL 或合理的默认值吗? 尤其是业务主键、排序字段。
类型选型就这么几类,核心是“够用、合适、不浪费”。一次认真设计,能避免后期大量修修补补;而一个类型选择失误,修复时往往是锁表、重建、数据迁移,代价远比创建表时多花五分钟大。