先聊个我印象很深的故障:那年一张线上订单表的累计状态值用的是 TINYINT,后来业务加了个新状态 299,当天半夜就开始疯狂报错,大量订单写不进去。排查半天,才发现不是程序 bug,是 TINYINT 有符号上限只有 127,你塞个 299 进去,严格模式下直接 Out of range。从那之后,我每次建表都会把 MySQL 数据类型里最容易出事的整数整明白:TINYINT、INT 和 BIGINT,谁也别想蒙混过关。这篇文章就是一次完整复盘,适合正在学 MySQL 的新人,也适合写业务代码的老手抽几分钟对照自查——很多问题不是不会写 SQL,而是建表那一刻类型就没选对。
MySQL 的整数类型永远不应该是“随手选一个”的事。TINYINT、INT、BIGINT 之间的差距,不只是名字长短,而是存储空间、取值范围、索引性能、甚至下游接口能不能接住的问题。下面我从底层差异讲起,把选型思路、实操细节、隐蔽坑点和排查手段一次说透。
1. 三种整数类型的核心差异:先从底层存储讲起
1.1 字节数、取值范围和内存成本,一张表算清楚
很多人知道 TINYINT 占 1 字节、INT 占 4 字节、BIGINT 占 8 字节,但从来没认真想过“为什么是这些范围”。这里有个最简单的算法:N 字节就是 8N 位,有符号类型最高位拿来做符号位,所以实际数值位是 8N-1 位。
- TINYINT:1 字节,8 位,有符号时最大值是 2^7 - 1 = 127,最小值是 -2^7 = -128。
- INT:4 字节,32 位,有符号时最大值是 2^31 - 1 = 2147483647,最小值是 -2147483648。
- BIGINT:8 字节,64 位,有符号时最大值是 2^63 - 1 = 9223372036854775807,最小值是 -9223372036854775808。
如果加上 UNSIGNED 关键字,最大值会往上翻一倍,下面是完整对照:
| 类型 | 字节数 | 有符号范围 | 无符号范围 | 建议用途 |
|---|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 0 ~ 255 | 状态、开关、小枚举 |
| SMALLINT | 2 | -32768 ~ 32767 | 0 ~ 65535 | 相对小范围计数 |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 0 ~ 16777215 | 中等数值 |
| INT | 4 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | 常规主键、ID、计数 |
| BIGINT | 8 | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 18446744073709551615 | 雪花ID、流水号、海量主键 |
这些范围不是我背出来的,而是可以在 MySQL 里直接验证的。拿 INT 举例,你建一张临时表,插入 2147483647 没问题,插入 2147483648 就会在严格模式下报错:
sql复制CREATE TABLE t_int_check (
id INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO t_int_check(id) VALUES (2147483647);
INSERT INTO t_int_check(id) VALUES (2147483648);
-- ERROR 1264 (22003): Out of range value for column 'id' at row 1
这里必须提醒一句:只有数字“刚好落在类型范围内”才是安全的,边界值很容易被忽略。曾经有同事把年份存在 TINYINT 里,等系统跨过 127 年当然不可能,但他存的是“用户年龄”也没问题,问题是他把“用户来源渠道编号”塞 TINYINT,渠道一多就爆。存什么值,决定你选什么类型,而不是反过来。
1.2 为什么不能为了省事全部用 BIGINT
有人会想:既然 BIGINT 范围最大,那不管什么字段都建 BIGINT 不就好了?我以前也这么干过,直到一张千万级流水表因为全字段 BIGINT 导致索引体积变大,查询性能和磁盘成本一起上来,才意识到问题没那么简单。
数据库的存储成本是“行数 × 字段字节数”放大出来的。假设一张表有 1 亿行,某个状态字段从 TINYINT(1 字节)升级成 BIGINT(8 字节),仅这个字段就多占 7 亿字节,约 670MB。如果表里有五六个整数字段,多出来的就是几个 GB 的量级。更关键的是,二级索引会复制主键值,主键越大,每个二级索引页能容纳的条目越少,随机 IO 更频繁,查询自然变慢。
你可以用一个查询对比同一张表下不同数据类型的实际差别:
sql复制SELECT
table_name,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS total_mb
FROM information_schema.tables
WHERE table_schema = 'your_db'
AND table_name IN ('t_
