实际项目里我见过不少因为整数类型选错而引发的线上事故。就拿主键来说,某平台早期用INT自增,业务跑起来之后主键一度逼近21亿,新增记录直接报错,紧急改表的那几个小时全组人都盯着监控屏。反过来,我也见过状态字段明明只有0和1两个取值,却用了BIGINT,一张千万级表白白多占几十GB存储。TINYINT、INT、BIGINT是MySQL里最常用的三种整数类型,但很多人对它们的理解只停留在“小号、中号、大号”这种模糊层面,字节数、取值范围、有符号无符号、显示宽度这些底层细节如果不吃透,建表时很容易拍脑袋决定。我先把选型思路、核心原理、实操步骤和复盘经验一条线讲清楚,不只是让你知道这三个类型差多少,更希望你在下一次写CREATE TABLE的时候,能下意识地算清楚“这个字段到底该用什么类型”。
1. 整数类型选型的整体思路拆解:TINYINT、INT、BIGINT 到底怎么选
1.1 为什么这三种整数类型值得单独掰开揉碎讲
日常开发里,表设计往往被压缩在几分钟内完成,业务逻辑还没理清就开始写CREATE TABLE。等到线上数据量上来了,慢查询、锁表、空间暴涨这些问题接踵而来,回头看根因,十有八九出在数据类型这一层。整数类型尤其容易被低估,因为它看起来太简单了,不过是一个装数字的容器,很少有人认真想过这个容器到底多大、够不够用、会不会浪费。
TINYINT、INT、BIGINT分别占1字节、4字节、8字节,单看一条记录差距确实不大,但数据库表就是拿来装海量数据的,一行差几个字节,千万行就是几十GB的差距,更别说二级索引里还会冗余存储主键值。整数类型的选择直接决定了存储成本、索引效率、查询性能,甚至决定了某个字段会不会在某一天突然溢出导致写入失败。先把这个“为什么要重视”的问题讲透,后面的实操方案才有意义。
1.2 三种类型的定位与适用场景:一张表看清全局
先上结论,三种整数类型的核心参数对比如下。
| 类型 | 字节数 | 有符号范围 | 无符号范围 | 典型场景 |
|---|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 0 ~ 255 | 状态码、布尔开关、星级评分、年龄、性别编码 |
| INT | 4 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | 常规主键、用户ID、订单流水、计数器 |
| BIGINT | 8 | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 18446744073709551615 | 分布式全局ID、雪花ID、外部系统大数值ID |
从这张表可以清楚看到,TINYINT适合小范围枚举和标记类字段,它能让单行更紧凑,同一条SQL扫描的数据页更少,性能收益实实在在。INT是绝大多数单库单表业务的默认选择,4字节对于现代CPU和索引结构来说很均衡。BIGINT则面向超大规模或必须保证绝对唯一的大数值场景,虽然空间代价最高,但可以为后续扩展留足余量。
具体到业务场景,性别、用户状态、订单状态、渠道编号、来源平台、星期几这类取值不超过几十个的字段,用TINYINT就够了;如果未来有对接外部系统的可能性,外部协议里定义了三位数编号,那就要评估SMALLINT而不是死守TINYINT。用户ID、订单ID这类会持续增长的业务标识,常规场景用INT没问题,但一旦涉及分布式环境多节点生成ID,或者需要和Redis自增、消息队列的全局序号对齐,直接BIGINT,别想着以后改。
1.3 选型背后的三个核心维度:容量、性能、扩展性
先讲容量。选整数类型不是看“现在够不够用”,而是看“未来够不够用”。我常用的估算办法是拿当前峰值乘以10,作为未来三年的安全余量,再对照取值范围表去选。比如当前用户量10万,三年后100万,INT绰绰有余;但如果你做的是物联网设备ID、埋点ID这类可能指数增长的数据,就要把余量放大到百倍千倍。
再讲性能。同样的记录数,字段字节越少,单行占用越小,InnoDB缓冲池能缓存的页就越多,B+树索引的扇出越大,全表扫描和范围查询的成本都会显著下降。一个只有小整数字段的表和一个塞满大整数字段的表,在同样的硬件条件下跑聚合查询,性能差距可能是量级的。
最后讲扩展性。选类型的隐形代价是“以后改字段类型有多难”。INT升BIGINT在合适的MySQL 8.0环境里可以秒级完成,但在低版本上往往要重建表,数据量大了就是几十分钟甚至几小时的锁表窗口。所以决策时要提前把“未来改动成本”算进去,能一步到位的别留尾巴。把这三个维度拆开想清楚,比单纯背类型范围表要实用得多。
2. 字节、范围与显示宽度:三种整数类型的核心细节
2.1 字节、位与取值范围的内在逻辑
很多人记不住三种类型的取值范围,其实背后就是一个简单的二进制换算。1字节等于8位,每一位是一个二进制位,能表示0或1,所以8位总共是2的8次方,共256种组合。有符号类型需要拿最高位当符号位,正数最大值就是2的7次方减1,等于127,负数最小值则是负的2的7次方,等于-128,加加减减正好覆盖256个值。
把这个逻辑推广到所有整数类型,就完全不用死记硬背。INT占4字节,也就是32位,有符号最大是2的31次方减1,等于21亿多;BIGINT占8字节,64位,有符号最大是2的63次方减1,大概是9.22乘以10的18次方。中间档位的SMALLINT(2字节)最大32767,MEDIUMINT(3字节)最大8388607,也都能靠这个公式心算出来。
再补充无符号的逻辑。UNSIGNED的意思是把原本用来表示符号的那一位也拿来存数据,所以正数上限会变成原来的两倍减一。TINYINT UNSIGNED最大255,INT UNSIGNED最大4294967295,BIGINT UNSIGNED最大18446744073709551615。什么时候用无符号?确认数据不可能为负,并且需要更多正数空间时。最典型的场景是自增主键、计数器、各种ID字段。注意,无符号不是“默认更安全”,它只是把负数空间让给了正数,选择时要想清楚业务里到底会不会出现负数。
2.2 显示宽度与ZEROFILL:历史上误导最多人的概念
MySQL建表时经常能看到int(11)、bigint(20)、tinyint(4)这样的写法,括号里的数字叫“显示宽度”,它在存储和取值上没有任何作用。int(1)和int(11)底层完全一样,都是4字节,取值范围完全相同。显示宽度仅在配合ZEROFILL属性时会影响查询展示:比如int(5) ZEROFILL存储的值为1,查询出来会显示00001,本质是给数字补零,让报表对齐更美观。
这个概念的误导性极强,网上大量老教程还在教int(11),新手很容易把int(1)理解成“只能存一位数”。实际上int(1)一样能存21亿,字段里出现上亿的数字完全不奇怪。更关键的是,MySQL 8.0.17开始,显示宽度语法已经被标记为废弃特性,新版本里写int(11)不会报错,但官方不再推荐。新项目里我建议直接写int,不写括号里的数字,干净且没有歧义。
2.3 有符号与无符号:选对了省空间,选错了出怪Bug
有符号和无符号的选择经常被忽视,默认情况下MySQL整数都是有符号的。无符号使用得当可以扩大正数范围,但也藏着两个典型坑,必须提前知道。
第一个坑是写入负数直接报错。MySQL 8.0对UNSIGNED列的约束比老版本严格,往无符号字段写入负数会直接报ERROR,老版本可能只是警告然后截断,行为不一致容易让跨版本迁移的项目出问题。第二个坑非常隐蔽:无符号字段参与减法运算时,如果结果小于0,MySQL不会返回负数,而是可能返回一个巨大的无符号整数。比如在MySQL 5.7上执行SELECT CAST(1 AS UNSIGNED) - CAST(2 AS UNSIGNED),返回的不是-1,而是18446744073709551615;到了8.0的严格模式下甚至可能直接抛错。库存扣减、余额计算、年龄差计算这些场景里,这种溢出最容易爆炸。
所以我的个人习惯是:常规业务字段大多数保持有符号,只有确认数据不会为负且需要更多正数空间时才用UNSIGNED。自增主键用UNSIGNED可以延长寿命,这是比较合理的场景;但如果有任何一个无符号列要在SQL里做减法,一定要显式CAST成SIGNED再计算。
2.4 布尔值在MySQL里的真面目
MySQL没有内置的布尔类型,写BOOL或BOOLEAN最终都会被转换成TINYINT(1)。很多ORM框架看到TINYINT(1)会自动映射成布尔类型,前端框架也经常把它渲染成勾选框,这带来一个认知错位:TINYINT(1)本质上是一个1字节整数,它完全可以存127,只是习惯上只用0和1来表示布尔语义。如果业务要求严格布尔属性,不能让应用层随意写入其他值,就需要在建表时加CHECK约束,MySQL 8.0.16开始CHECK才真正强制生效。
CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, is_active TINYINT(1) NOT NULL DEFAULT 1, PRIMARY KEY (id), CONSTRAINT chk_is_active CHECK (is_active IN (0, 1)) ) ENGINE=InnoDB;这样做的价值是数据库层面兜底,应用层就算传了2进来也会被拒绝。如果只是普通枚举状态字段,也建议用语义更明确的TINYINT而不是裸写TINYINT(1),避免客户端和ORM误判。
2.5 别把SMALLINT和MEDIUMINT这两个中间档位忘了
虽然这篇文章的主角是TINYINT、INT、BIGINT,但选型时MySQL的整数家族里还有两个中间档:SMALLINT占2字节,有符号最大32767,无符号最大65535;MEDIUMINT占3字节,有符号最大8388607,无符号最大16777215。它们的存在价值是填补“TINYINT不够用,INT又太大”的空档。
举个例子,省份编码、端口号、业务错误码这类取值在几百到几万之间的字段,用SMALLINT比INT节省2字节,一张千万级表就能省20MB以上。订单量在百万到千万级别的流水号,MEDIUMINT也常常够用。不过要注意,中间档位的类型在ORM和语言层的映射不如INT、BIGINT那么顺滑,有些框架可能不支持MEDIUMINT,所以要兼顾开发效率,不能为了省空间把团队折腾得很难受。
3. 建表实操:从选型到改表,让每个整数列都各归其位
3.1 第一步:明确字段语义,给每个整数列打标签
动手建表前,我习惯把所有整数列先分类。一般分四类:标识类,指主键、外键、业务编号,关注唯一性和未来总量;枚举类,指状态、类型、渠道、等级,看枚举值个数和扩展空间;度量类,指数量、次数、年龄、金额的整数部分,注意量级和运算方式;标记类,指布尔开关、有无标志,能小则小。分类不是形式主义,每类字段的选型逻辑完全不同,后面的步骤都是在这个基础上展开。
这个分类动作看起来简单,但它直接决定选型方向。同一个值域很窄的字段,如果它是枚举类,TINYINT足够;如果未来可能对接外部协议、取值范围被外部放大,那就要宽容一些。给字段打标签的过程,其实也是把业务约束重新梳理一遍的过程,很多建表时的“随手拍”都是因为跳过了这一步。
3.2 第二步:估算数据量,把上限算出来再选类型
选类型最怕拍脑袋,我通常按“当前峰值乘以10”作为未来三年的安全余量。当前用户量10万,三年后即使涨到100万,用户ID用INT也绝对够;但如果做的是埋点明细表,一天就是上千万的写入量,一年之后总量就是几十亿,那主键或业务ID就得认真考虑BIGINT了。
估算还有一个细节:不要只盯着行数,要把索引因素算进来。比如一张表的主键用INT,二级索引有5个,那么每行存储时主键值会被聚簇索引和5个二级索引各记一份,一亿行就是不小的索引空间。如果换成BIGINT,这个数字会明显变大。所以“够用”的定义里,除了直观看数据量的上限,还要考虑它对整个索引体系的空间放大效应。
3.3 第三步:主键类型单独决策,不要随大流
主键是全表访问最频繁的字段,InnoDB的聚簇索引就建立在主键上,所有二级索引的叶子节点又都会冗余保存主键值,所以主键类型的选择会影响整张表的存储和索引。常规单库单表业务,INT无符号主键上限42亿,对绝大多数用户、订单类应用都够用;但一旦涉及分布式ID、雪花ID、跨系统传递ID,或者明确知道表量级会冲到亿级、十亿级,直接BIGINT,不要犹豫。
这里说一个常见的反面案例:有些团队觉得“主键用INT够了,以后真的爆了再改”,可真到爆的那天,改主键类型往往要重建整个索引体系,在低版本MySQL上就是一次锁表时间极长的DDL。相比之下,建表时直接选BIGINT的成本几乎可以忽略。所以我的经验是:拿不准主键用INT还是BIGINT时,优先BIGINT。
3.4 第四步:AUTO_INCREMENT与索引配合时的细节
自增列必须定义在某个索引上,通常就是主键。TINYINT、INT、BIGINT都可以作为自增类型,自增上限和类型上限一致,一旦撞上上限,新增记录会报Duplicate entry。很多人只考虑数据库层的自增上限,忽略了应用语言层的类型匹配:MySQL的BIGINT上限是9223372036854775807,超过Java的int范围,如果Java代码里用int接收主键,数据库层还没到上限,应用层就先溢出了。
另外,InnoDB在MySQL 8.0下的默认innodb_autoinc_lock_mode=2,批量插入时一次性申请一段自增值,如果事务回滚或中途失败,这段值会被消耗掉,导致自增ID出现空洞,比如连续插入几条之后,下一条直接跳到100。这是正常行为,不要当成故障去排查。早期5.7版本对自增锁的持有更保守,但8.0已经彻底优化掉了批量插入时的性能瓶颈。
3.5 第五步:用SQL把字段类型查个明明白白
判断线上某张表的字段到底用的什么类型,最直接的命令是SHOW CREATE TABLE;要批量排查几十张表,INFORMATION_SCHEMA是更好的选择。下面这个查询可以直接列出某个数据库里所有表的整数列详情:
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'your_db' AND DATA_TYPE IN ('tinyint', 'smallint', 'mediumint', 'int', 'bigint') ORDER BY TABLE_NAME, ORDINAL_POSITION;在Navicat这类图形客户端里,打开表设计器也能看到完整类型,但手动一张张点效率太低。我排查“哪些表用了BIGINT却不合理”“哪些状态字段用了INT”这类问题时,就是用上面的SQL跑一遍,再结合业务逐张确认,比界面操作高效得多。
3.6 从INT升级到BIGINT的平稳迁移方案
如果线上确实需要把INT主键升级成BIGINT,方案要选对。MySQL 8.0提供了ALGORITHM=INSTANT选项,某些纯类型扩展在满足条件时不需要重建表,可以做到元数据级修改,速度很快。可以这样执行:
ALTER TABLE order_record ALGORITHM=INSTANT, MODIFY COLUMN id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT;不过INSTANT算法有前置条件,比如表结构、版本、行格式都会影响是否支持。执行时如果MySQL弹出了类似“ALGORITHM=INSTANT is not supported”的错误,就说明这张表不能用这个方案,会自动放弃而不是静默回退,这时就要改用其他方式。在低版本或超大表场景,我更推荐配合在线改表工具,或者用影子表迁移:建新表、导数据、切换应用连接。核心原则是,大表DDL永远要在低峰期执行,并且操作前必须留有可回滚的备份。
4. 线上案例复盘:那些年整数类型挖过的坑
4.1 事故现场:INT主键自增溢出
前面提到的线上事故再展开讲讲。某平台订单表主键用INT,默认有符号,上限2147483647。业务跑了大概三年,订单量持续增长,某天下午日志里突然出现大量Duplicate entry '2147483647' for key 'PRIMARY',新增订单全部失败,用户下单入口直接瘫痪。
排查步骤很明确:先看SHOW TABLE STATUS里Auto_increment列,发现已经逼近2147483647;再查当前最大ID确认临界状态;最后定位是主键类型太小导致的容量危机。处理方案是低峰期把主键改成BIGINT UNSIGNED,合适的MySQL 8.0环境可以用INSTANT算法快速完成;如果版本不够或表结构不支持,就得走在线改表工具或者影子表方式,整个流程可能要数小时。这个案例的教训是:主键类型不是“先凑合后面再改”的决策,尤其是订单、流水这类只增不减的表,它的增长速度往往比业务预估快得多。
4.2 隐式类型转换:索引失效的隐形杀手
慢查询排查中,我碰到过不少“明明有索引却不用”的案例,根因就是隐式类型转换。MySQL在比较不同类型值时会把一边转成数字或字符串,一旦转换发生在索引列上,索引就失效了。方向不同,影响也不同。字段是BIGINT、查询条件是字符串,比如WHERE id = '100123',MySQL通常会把字符串转成数字,索引还能用;反过来,如果字段是varchar、存了一串数字,条件是WHERE user_no = 100123,MySQL会把该列全部转成数字再比较,索引基本失效,全表扫描。
这种情况用EXPLAIN一眼就能确认:type列变成ALL,possible_keys里有索引但实际没用上。修复方式是让两边的类型统一,最干净的是从应用层把参数写成字段相同的类型。我也用过SHOW WARNINGS来查看MySQL具体做了什么类型的转换,定位更快。这个坑在接口层传参比较随意时特别常见,排查慢SQL时多留个心眼。
4.3 TINYINT(1)与布尔勾选框的坑
图形化客户端打开带TINYINT(1)字段的表时,经常会看到一个勾选框,这是客户端根据显示宽度猜测的布尔渲染。这种视觉误导很危险,DBA在表设计器里只是点了一下,可能就把0改成了1,或者把状态勾掉,业务数据就变了。有些ORM看到TINYINT(1)也会自动映射成Boolean,如果一个字段业务上实际有0、1、2三个状态,映射到Boolean之后,2就会被当成true,数据语义直接错乱。
避坑建议很明确:真正存布尔值的字段才用TINYINT(1),并且要加注释说明;需要存多状态枚举时,用TINYINT(4)或者直接TINYINT,不给客户端和ORM误解的机会。如果已经建立了大量TINYINT(1)表,改结构成本又高,那就在应用层做严格校验,把非法值挡在业务入口。
4.4 无符号相减出来的天文数字
无符号整数做减法溢出这个问题,我在报表SQL里踩过一次。当时是一张库存表,current_stock和locked_stock都定义的UNSIGNED INT,报表里直接算available_stock = current_stock - locked_stock。平时没问题,某次上游数据错误导致被减数小于减数,结果不但没有出现负数,反而出了一个40多亿的天文数字,报表直接失真,排查了半天才定位到这个溢出逻辑。
从那以后,凡是涉及减法、甚至任何可能产生负数的计算,我都会在SQL里显式CAST成SIGNED,或者用CASE WHEN做保护。比如:
SELECT CASE WHEN current_stock >= locked_stock THEN current_stock - locked_stock ELSE 0 END AS available_stock FROM inventory;这个教训提醒我:无符号类型可以安全地存数据,但它不适合参与默认的数值运算,尤其不要让两个无符号字段直接做减法。
4.5 整数类型问题速查表
整理一个常见问题速查表,线上遇到问题可以直接对号入座。
| 现象 | 根因 | 排查命令 | 解决方案 |
|---|---|---|---|
| 新增记录报Duplicate entry '2147483647' | INT自增溢出 | SHOW TABLE STATUS LIKE 'order_record' | 升级BIGINT或影子表迁移 |
| 查询极慢,EXPLAIN显示ALL | 隐式类型转换 | EXPLAIN + SHOW WARNINGS | 统一字段与查询条件的类型 |
| 表设计器出现布尔勾选框 | TINYINT(1) | SHOW CREATE TABLE | 改用TINYINT(4)或加CHECK约束 |
| SQL减法结果出现天文数字 | UNSIGNED溢出 | SELECT直接复算表达式 | CAST为SIGNED或CASE WHEN保护 |
| 批量插入后自增ID不连续 | innodb_autoinc_lock_mode=2的正常行为 | SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode' | 属于预期行为,无需处理 |
这张速查表我是按线上真实排障顺序整理的,每个案例背后都是一次完整的排查过程。遇到类似问题时,建议先别急着优化SQL,先确认字段类型定义是否合理。我见过太多人花一下午调SQL,最后发现索引失效的根源其实是字段类型和查询参数类型不一致;方向定对了,排障效率能提升一大截。
4.6 建表时就把坑堵住的检查清单
最后给一份可执行的自检清单,每次建表前过一遍:
- 状态、标记类字段是否已经最小化,能不能收敛到TINYINT?
- 标识类字段是否按“当前峰值乘10”估算过未来三年的量级?
- 主键是否考虑过二级索引带来的空间放大效应?
- 新项目是否还在写int(11)这种显示宽度语法?如果是,删掉。
- 无符号字段会不会参与减法或负数运算?如果有,改成有符号或加保护逻辑。
- 自增列的类型是否和上层语言(Java、C#、Go)的类型范围匹配?
- 表的注释里是否写清楚了TINYINT(1)这类字段的业务含义?
这条清单是我自己Review表结构时的底稿,按它过一遍,大部分整数类型相关的坑都能在建表前被挡住。配合前面讲的INFORMATION_SCHEMA查询批量扫描,线上老表也能逐步清理。
最后聊一点我自己的操作习惯。这几年经手过的表设计,我基本遵循一条原则:能用TINYINT表达的状态绝不上INT,只要涉及分布式ID或者表量级有可能冲到千万以上,主键直接BIGINT,不在选型上反复纠结。还有一个习惯,每次建完表我会顺手用INFORMATION_SCHEMA查一遍新表的全部字段类型和注释,把所有“看着差不多就选了”的地方找出来改掉。整数类型本身不复杂,但它挂在表设计、索引效率、业务增长、DDL变更这一整条链路上,选错一次的代价远超选大一点。希望这篇内容能帮你把以后踩坑的时间省下来,至少在写下一次CREATE TABLE时,多花一分钟把字段类型算清楚。