
刚接触 MySQL 的时候很多人的态度就是反正都是整数用 INT 准没错。等到表建多了、数据量上来了才发现事情没这么简单。同样是整数TINYINT、INT、BIGINT 的存储空间、取值范围、甚至对索引的影响差别都不小。选错了类型轻则白白浪费磁盘重则表上线一年多就插不进数据最后只能顶着大流量做 ALTER TABLE 重建。这篇内容我打算把三种整数类型彻底掰开讲清楚取值范围怎么来的、INT(11) 那个括号到底意味着什么、主键选 INT 还是 BIGINT、为什么状态字段建议用 TINYINT、以及因为类型选错引发的一堆线上故障。无论你是刚入门想搞懂 MySQL 基础还是已经写过几年 SQL 想回头补一补底层的选型逻辑这篇文章应该都能给你点启发。全程不涉及复杂源码但我会把关键行为讲透保证你看完能直接拿去指导表结构设计。1. 三种整数类型到底差在哪取值范围与存储结构先说最基本的结论TINYINT 占用 1 字节INT 占用 4 字节BIGINT 占用 8 字节。字节数决定了取值范围也决定了每行数据在磁盘和内存里占多大位置。很多人记不住范围其实只要理解了字节和位的关系三者的范围可以直接推算出来完全不用死记硬背。1.1 一张表看清 MySQL 全部整数类型MySQL 其实提供了 5 种整数类型除了标题里的三种还有 SMALLINT 和 MEDIUMINT 两个中间档。把它们放在一起看更清楚类型字节数位数有符号范围无符号范围TINYINT18-128 ~ 1270 ~ 255SMALLINT216-32768 ~ 327670 ~ 65535MEDIUMINT324-8388608 ~ 83886070 ~ 16777215INT432-2147483648 ~ 21474836470 ~ 4294967295BIGINT864-9223372036854775808 ~ 92233720368547758070 ~ 18446744073709551615我把 MEDIUMINT 也放进来的原因很简单它虽然不如 INT 和 BIGINT 常见但在某些传统系统里一年流水量不太可能超过 1600 万的中间表用 MEDIUMINT 很合适3 字节存储比 INT 省 1 字节。不过现代的 MySQL 8.0 表设计里我会更推荐直接用 INT省那一字节带来的复杂度远大于收益。所以后面的讨论只针对标题里的 TINYINT、INT 和 BIGINT。1.2 为什么一个字节的 TINYINT 上限是 127一个字节有 8 个 bit每个 bit 能表示 0 或 1那么一个字节总共能表示 2 的 8 次方等于 256 种组合。如果有符号类型需要把其中一位拿出来做符号位那么真正存数值大小的就只剩 7 位最多表示 2 的 7 次方等于 128 种大小。正常情况下这 128 种组合一半给负数一半给正数从 -128 到 127 正好 256 个数。你可以把这一位符号位想象成钱的“正负号”符号位本身不参与大小计算只是告诉 MySQL 这个数是正还是负。所以无符号 TINYINT 把符号位省下来就直接从 0 到 255。这就是为什么同样 1 字节TINYINT 有符号和无符号的取值上限差了快一倍。INT 和 BIGINT 也是同样的逻辑INT 用 31 位存数值BIGINT 用 63 位存数值。我有一次面试候选人问到 INT 的最大值为什么是 2 的 31 次方减 1很多人答不上来其实只要理解“最高位做符号位剩下 31 位算数值减 1 是因为要从 0 开始计数”这句话就够了。这个知识点看起来很基础但在排查数据溢出问题的时候能帮你一眼看出问题根源在哪。1.3 别让 UNSIGNED 帮倒忙无符号和有符号的取舍是最容易出问题的地方。很多教程喜欢推荐“主键用 INT UNSIGNED这样范围能从 21 亿变 42 亿”这句话本身没错但如果你的 SQL 里有减法、负数或跨类型比较UNSIGNED 就会变成一颗雷。举个线上常见的场景某个表用 INT UNSIGNED 存库存你写了一条UPDATE stock SET num num - 5 WHERE id 1结果 num 当前是 3按道理库存不足应该报业务错误但 MySQL 在严格模式下会直接抛出一个报错报错词大概是BIGINT UNSIGNED value is out of range。为什么会提到 BIGINT因为在计算过程中 MySQL 会把 UNSIGNED INT 提升为 UNSIGNED BIGINT 再运算一旦减出负数就溢出了。我的建议是如果业务上确实需要更大的正数范围优先考虑 BIGINT而不是 INT UNSIGNED。原因有两个一是 BIGINT 是有符号的运算时不会出现 UNSIGNED 的减法陷阱二是很多 ORM 框架和编程语言对无符号整数的支持并不友好Javascript 里超过 2 的 53 次方就丢精度了存 BIGINT 的 ID 已经够你头疼的再来个 UNSIGNED前后端对接时更容易出问题。2. 显示宽度INT(11) 里那个括号到底在表达什么我敢说MySQL 使用者中一大半都误解过INT(11)里的数字。有人以为它限制最大长度只能存 11 位有人以为它是存储大小的配置还有人为了“让字段存更多位数”专门写INT(20)。这些理解都是错的。2.1 显示宽度不限制存储大小INT(11)里的 11 在 MySQL 5.x 时代被称为“显示宽度”。它的唯一作用是在没有写入任何数据时用 0 或空格去填充这个字段在结果集里的呈现宽度前提是字段启用了 ZEROFILL。它既不限制你能存储的最大值也不占用额外的存储空间。换句话说INT(11) 和 INT(2) 在存储层面是完全相同的INT(2) 照样能存 21 亿。很多老项目的建表语句里能看到int(11)是因为 MySQL 5.7 及更早版本的一些客户端工具在生成 DDL 时会自动补上这个括号。时间一长大家就误以为这是必需语法。实际上你在 MySQL 8.0 里写CREATE TABLE t (id INT)SQL 不会报错也不会生成多余括号。2.2 ZEROFILL显示宽度的唯一实际用途显示宽度真正产生可感知效果的场景是配合 ZEROFILL 属性使用。比如你定义一个字段code INT(5) ZEROFILL插入数值 1查询出来会显示为00001插入 12345 还是 12345插入 123456 就会显示完整 6 位因为数据本身已经超过宽度了。CREATE TABLE t_zerofill ( code INT(5) ZEROFILL ); INSERT INTO t_zerofill VALUES (1), (12345), (123456); SELECT code FROM t_zerofill; -- 00001 -- 12345 -- 123456说实话这个功能对大多数业务系统没有价值。它只是在展示层做填充数据在物理存储上依然是原值。而且你要记住一个隐藏坑ZEROFILL 属性会隐式把字段设置为 UNSIGNED。也就是说你写TINYINT ZEROFILL它自动就变成无符号只能存 0 到 255不能存负数。不少新手在这个位置踩过坑建表的时候没注意后面插入负数直接报错。2.3 MySQL 8.0 已经对显示宽度说再见了MySQL 8.0 开始官方已经明确把整数类型的显示宽度标记为废弃特性8.0.19 以后你写INT(11)也不会按照“显示宽度”来处理了。这意味着新建项目完全不用再纠结 int(11)、init(4) 之类的写法直接写 TINYINT、INT、BIGINT 就好。老项目维护时倒是还有机会看到这种 DDL遇到别慌不影响数据也基本不需要专门做一次迁移。我给新人的建议是建表语句越简单越清晰越好不带括号、不带 ZEROFILL、不过度配置就是一个类型名加 NOT NULL 和 COMMENT。这样别人看你的表结构时注意力会放在业务语义上而不是花时间琢磨这个括号是不是有什么特殊意图。3. 不同场景该怎么选字段选型与实际案例接下来是很多人真正关心的部分实际业务里到底怎么选。这里我不会给出一刀切的答案因为选型依赖数据量、并发量、未来扩展空间和团队习惯。但我会给你一套判断方法和几个高频场景的推荐方案。3.1 状态字段TINYINT 的标配订单状态、用户状态、是否删除、是否启用、审核结果这类字段究其本质就是一个短枚举取值范围非常有限。0 表示待处理1 表示处理中2 表示已完成这类的状态数量大概率不会超过 20 个。用 TINYINT 是最合理的选择1 字节存储检索快占空间小索引也轻量。CREATE TABLE demo_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已取消 3-已退款, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;MySQL 里的 BOOLEAN 和 BOOL 其实也是 TINYINT(1) 的别名存 0 和 1。如果你的状态只有真和假两种直接用TINYINT(1)语义更清晰。MySQL 8.0.16 以后还可以加 CHECK 约束限制状态范围比如CHECK (status IN (0,1,2,3))给这层短枚举再加一道防线。3.2 自增主键INT 还是 BIGINT先算账再拍板自增主键选 INT 还是 BIGINT核心看两件事当前表多少数据、业务生命周期内能长到多少数据。INT 有符号上限约 21.47 亿无符号约 42.94 亿。如果一张表用有符号 INT 自增主键每天新增 10 万条大概 5.8 年就到了上限。每天新增 100 万条大概 7 个月就顶到头。对于很多中小业务来说不是“够不够”的问题而是“有没有预留应对突然增长的空间”。所以我的建议是日增量在 1 万以下业务生命周期内总量预计几千万级用 INT 足够。日增量在 10 万以上或者你有电商订单、日志流水、用户关系等大表潜质的直接上 BIGINT。不加 UNSIGNED 时 INT 只能到 21 亿很多团队干脆统一用 BIGINT避免未来迁移。这里特别提一句把 INT 改成 BIGINT 不是改一个字段类型那么简单。在 InnoDB 里修改主键数据类型会导致整个聚簇索引重建数据量大的时候这个操作非常耗时还会对主库产生不小的锁压力。换句话说自增主键的类型选错后期修复成本比你想象的高得多。宁可一开始就选保守偏大的 BIGINT。3.3 分布式 ID、雪花 ID为什么必须 BIGINT如果你所在的公司是微服务架构或者未来可能要搞分库分表主键大概率不会是数据库自增 ID而会采用雪花 ID 一类的分布式 ID 生成方案。雪花 ID 本质上是一个 64 位的长整型Java 里通常用 long 表示对应数据库就是 BIGINT。这种情况下你根本没有第二个选择只能用 BIGINT。有人会问能不能把雪花 ID 存在字符串里能但不推荐。字符串比较需要逐字节对比存储空间也更大对索引性能不利。既然生成端是 64 位整数数据库端就直接用 BIGINT 承接不要去搞类型转换少一道转换就少一类坑。3.4 慎用 BIT、BOOL 等别名类型MySQL 还提供 BIT 类型有人觉得 BIT 比 TINYINT 更省空间存 0 和 1 最合适。理论上是这样但实际开发里我不推荐。BIT 类型在程序端取出来会变成二进制字节串很多 ORM 和 JDBC 驱动处理起来比较别扭查出来可能是一堆不可见字符。相比之下 TINYINT 是标准的整数所有语言都能平滑对接。同理布尔字段不要用 INT 去存一个 4 字节的 INT 存 0 和 1纯粹是浪费。这种“小字段大类型”的问题单独看无所谓但一张宽表里有十几个布尔字段每个多 3 字节百万行就是 30MB 的冗余再加上二级索引还要复制一份主键值浪费会进一步放大。积少成多别小看这点空间。4. 整数类型与索引性能选择不当的隐藏代价很多人以为字段类型只影响存储大小这是一个危险的误解。整数类型的宽度还会直接影响 InnoDB 索引树的性能表现尤其是主键类型的选择往往决定了整棵聚簇索引的结构和查询效率。4.1 InnoDB 索引扇出主键越大树越深InnoDB 的默认页大小是 16KB也就是 16384 字节。B 树非叶子节点里每一行目录项大致存储一个键值和一个指向子页的指针。我做一个粗略估算如果主键是 INT4 字节键值加约 6 字节指针一共约 10 字节那么一个 16KB 页大概能容纳 1638 条目录项如果主键是 BIGINT8 字节键值加 6 字节指针一共约 14 字节一个页大概只能容纳 1170 条目录项。这意味着什么在同样数据量下使用 BIGINT 主键的 B 树扇出会更小树可能会比 INT 主键的树更高一层。B 树每深一层查询就要多一次磁盘 IO。对于内存命中率很高的热数据这点差异也许不明显但对于数据量几千万上亿、访问模式分散的表多一次磁盘 IO 的影响就被放大了。性能测试里有一个很常见的现象同样的表结构和数据量把主键从 BIGINT 改成 INT 后某些范围扫描查询明显变快。这不是玄学核心原因就是主键列参与所有二级索引的构建主键越大每个二级索引叶子节点存的值也越大整棵树的空间和 IO 开销都会增加。4.2 隐式类型转换让索引失效的元凶类型选错还容易引发隐式类型转换。比如你有一个字段code VARCHAR(20)里面存的都是数字字符串查询时写成WHERE code 123456MySQL 会把字符串列转换为数字去做比较。一旦对索引列做了函数或类型转换索引就会失效导致全表扫描。我见过一个真实的慢查询案例一张几百万行的订单表order_no 字段是 VARCHAR 类型程序里查询用了where order_no 202211150001这种不带引号的数字写法。数据库每秒钟产生大量全表扫描CPU 被打满。改成where order_no 202211150001之后查询从几秒钟降到几十毫秒。这个问题的根源就是字符串类型被拿来和数字比较触发隐式转换索引彻底失效。反过来如果字段本身是 INT查询条件写成字符串WHERE id 123MySQL 会把字符串转成数字再比较索引还能正常使用。但为了代码一致性和减少认知负担我仍然建议让查询参数类型和字段类型保持严格一致。ORM 框架里的数字参数不要传字符串字符串参数也不要传数字。4.3 JOIN 条件类型不一致慢查询的常见根源隐式转换不只出现在 WHERE 条件里多表 JOIN 时字段类型不一致也会引发。比如 A 表的 user_id 是 INTB 表的 user_id 是 BIGINT两张表关联时 MySQL 需要把其中一列做类型转换这会让 JOIN 无法高效使用索引或者只能先转换完再关联。这类问题在慢查询日志里很常见特征是多表关联的表都没多大但是查询时间就是上不去。排查方法也很简单在 EXPLAIN 里看 type 是不是变成 ALL 或 index或者 Extra 里有没有出现Using where的异常提示。比较专业的做法是统一所有表的主外键类型用户 ID 在哪个表都用 BIGINT订单 ID 在哪个表都用 BIGINT不要出现一个表 INT 另一个表 BIGINT 的情况。这种统一规范属于团队约定短期内看不到明显收益但长期维护高复杂度的数据库时能省很多排查时间。4.4 数值精度与运算陷阱整数类型还有一个特点是“整数”不要拿它存小数和金额。金额建议用 DECIMAL比如DECIMAL(10,2)。分账、对账、统计报表这类场景对精度要求极高直接用 FLOAT 或 DOUBLE 会产生不可控的精度误差这在财务系统里属于事故级问题。哪怕你金额最小单位是分也建议用 DECIMAL 或 BIGINT 存“分”避免浮点误差。这是一个老生常谈的建议但每次谈都有人在评论区里分享自己被精度问题坑过的经历。5. 常见报错与排查技巧实录最后这部分我把实际项目中因为 TINYINT、INT、BIGINT 选型引发的高频问题整理成速查再配上排查思路。这些都是我在平时运维和开发中反复看到的。5.1 Out of range value for column严格模式的保护最典型的报错是Out of range value for column status。比如status TINYINT的取值范围是 -128 到 127你往里面插入 300MySQL 5.7 之后的默认严格模式会直接拒绝本次写入。如果早期 MySQL 配置把严格模式关掉了MySQL 会改为插入 127并留下一行 Warning。这种“假成功”比直接报错更可怕因为业务代码里完全察觉不到数据已被截断。排查和修复思路先确认字段取值范围是否匹配业务状态数量超过一百个的本身设计可能就有问题。需要大范围时把 TINYINT 调整为 SMALLINT 或 INT而不是去修改 sql_mode 关掉严格模式。查看数据库错误日志时注意区分是截断警告还是实际写入失败。5.2 BIGINT UNSIGNED value is out of rangeUNSIGNED 的减法坑这个报错我在前面提过具体场景通常是两个 UNSIGNED 整数相减而结果恰好为负数。MySQL 为了保证最终结果不会变成负数会把它提升为更高精度的类型但计算后仍然发现结果超出 UNSIGNED BIGINT 范围于是报错。典型例子的优化办法是把库存、余额这类可能出现负数的运算统一改成“先判断再扣减”或者直接在字段层改用更有业务含义的余量字段。如果你只是希望加个负数标记不如一开始就不要用 UNSIGNED。这个坑我在一个仓储系统里遇到过当时查了很久才意识到是 UNSIGNED 惹的祸。5.3 修改已有列的类型别忽视重建表的代价很多人在上线后才发现字段类型不够用然后跑一条ALTER TABLE t MODIFY COLUMN id BIGINT。在数据量小的表上这没什么感觉但到了千万级数据的表上这类 DDL 会重建整张表耗时可能长达几十分钟甚至更久期间对表的读写还可能被锁住。MySQL 8.0 引入了 INSTANT DDL但并非所有类型修改都支持 INSTANT。把 INT 改成 BIGINT 属于需要重建表的操作。所以上线前把类型判断好的价值就在这里后期迁移的代价通常比想象中大得多。改成 BIGINT 时还有另一个细节如果已经有数据旧表里 INT 的最大值一定小于 21.47 亿迁移过程本身不会溢出差错但新表列的类型变更会让所有二级索引跟着重建。如果有几个大索引操作时间会进一步拉长。建议在业务低谷窗口执行并先在测试环境用相同数据量预估耗时。5.4 整数类型选型速查表业务场景推荐类型核心理由布尔值、短状态枚举TINYINT1 字节足够语义清晰常规自增主键、普通计数INT覆盖亿级以内数据索引更轻量大表主键、分布式 ID、流水号BIGINT范围大避免迁移手机号、证件号等VARCHAR不参与运算避免精度问题金额DECIMAL精度可控避免浮点误差负数参与运算的字段避免使用 UNSIGNED防止减法溢出报错这张表可以当作建表时的快速参考但真正的选型还得结合你对业务增长的判断。我的经验是初期的表结构设计花 10 分钟去核算字段类型后面能省下不只 10 小时。尤其是“省空间”的诱惑要抵抗住比如为了省 3 个字节用 MEDIUMINT结果业务爆发后做迁移绝对得不偿失。TINYINT、INT、BIGINT 这三个类型是最稳妥的组合绝大多数业务都能在里面找到答案。我个人在实际操作中的体会是与其反复背取值范围不如把每一类字段当成一个“容器”先问自己能装多少数据再问业务会不会超过这个量最后才动手建表。状态和布尔交给 TINYINT常规主键用 INT所有可能走向高并发、海量数据或者分布式场景的标识字段直接上 BIGINT。这样设计出的表结构干净、抗风险给未来的自己和同事都省心。