先问个问题:建表的时候看到int(10),你的第一反应是什么?我猜不少人和我一样,一开始以为它和varchar(10)类似,表示这个字段最多只能存 10 个字符,所以int(1)就只能存一位数字。这个理解错得相当离谱,但流传又特别广。做 MySQL 面试题收集久了你会发现,int(1)和int(10)的区别几乎是必问基础题,也是最能看出一个人到底有没有真正落地写过表的分水岭。
今天不绕弯子,直接把结论放在最前面:在 MySQL 里,int(1)和int(10)在存储、取值范围、索引、排序等任何底层行为上完全一致,它们都是同一个int类型,都占 4 个字节。括号里的数字既不是长度限制,也不是位数限制,它只是 MySQL 里的一个“显示宽度”概念。这篇文章主要面向刚接触 MySQL 的同学,也能帮自以为懂的老手梳理一下版本演进带来的变化,尤其是 MySQL 8.0.17 之后官方对显示宽度的处理方式,很多人还停在旧版本的认知里。
1. 先搞清楚:int(1) 和 int(10) 到底在比什么
1.1 括号里的 M 是显示宽度,不是存储长度
先说一个最容易混淆的点。varchar(10)里的 10 是字符长度,限制这个字符串最多存 10 个字符,你写第 11 个字符就会报错。但int(M)里的 M 全称叫 display width,翻译过来是“显示宽度”,它不是存储长度,更不是取值范围。int 类型在 MySQL 内部是固定长度,永远是 4 个字节,你写不写括号、括号里写 1 还是写 10,甚至写 255,磁盘上占的空间都一样。
可以这么理解:int像是一个固定大小的容器,容量从一开始就焊死了,4 字节能装多少就装多少。括号里的数字只是给这个容器外面贴的一张标签,告诉客户端“如果要用零填充显示,请补到几位”。这张标签改变不了容器本身的大小,就像你在一个 4 升的桶上贴了“1L”或者“10L”的贴纸,桶还是那个桶,能装的水还是 4 升,区别仅仅是有人看贴纸时可能会误以为桶的容量变了。
还有一点值得记住,显示宽度在 MySQL 5.7 及更早版本里是有上限的,最大是 255。你写int(256),在旧版本里会直接报错,提示 display width out of range。但在 MySQL 8.0.19 之后这个限制基本失去意义,因为显示宽度已经被官方废弃了,具体废弃逻辑放到后面第 4 节细说。
1.2 存储和取值范围,两个完全一样
这一节是很多人踩坑的根源。int(1)和int(10)既然都是 int,那取值范围就完全由 int 这个类型决定。有符号情况下,int 的取值范围是-2147483648到2147483647;无符号情况下,取值范围是0到4294967295。
换句话说,int(1)完全可以存储2147483647这个十位数,它在存储层面没有任何限制。有些项目里字段写成int(1),业务方就认为“这个字段只能存 0 到 9”,结果某个金额超过 9 就报错,最后排查半天发现不是数据问题,而是当初建表的人被这个括号骗了。这种问题在真实项目里出现过很多次,尤其是从网上复制建表语句时特别容易留下这种历史包袱。
再补充一点性能层面的结论:int(1)和int(10)的索引大小、索引比较代价、排序代价、JOIN 代价完全一样,因为它们本质上就是同一个数据类型。你给int(1)建索引和给int(10)建索引,没有任何区别,不要觉得位数写小一点索引就更快,这是不存在的。
1.3 这个错觉是怎么流传开来的
既然显示宽度这么容易误导人,为什么还会被大量写进建表语句?我总结下来主要有三个来源。
最直接的原因是把varchar的习惯迁移到了int上。很多人学 MySQL 时先学的字符串类型,知道括号里的数字是最大长度,于是看到别人的表里有int(10),想当然地以为这也是长度限制。网上大量“从零开始学 MySQL”的教程和问答里也充斥着类似说法,一旦流传开,新手很难辨别。
第二个来源是图形化工具。Navicat、MySQL Workbench 这些工具在导出建表语句时,经常给你生成int(10)或者int(11)这样的写法。尤其是int(11),这个数字其实是有来历的:int 有符号类型能存储最小值-2147483648,算上负号一共 11 个字符,所以显示宽度取 11 就是为了完整显示这个最小值。int(10) unsigned也类似,因为无符号 int 的最大值4294967295刚好是 10 位。工具自动生成不代表这就是业务设计建议,它只是照着元数据里的显示宽度原样导出而已。
第三个来源就是历史习惯。早期 MySQL 版本里,官方文档和不少经典书籍里的示例表都写着int(11),于是一代代开发者照抄。抄的人不知道为什么要写 11,但也不敢随便改,最后这种写法就成了“行业潜规则”。你问他们为什么写int(10),最常见的回答就是“看别人都这么写”。
2. int(M) 唯一显眼的作用:zerofill 补零
2.1 不加 zerofill,M 基本就是个摆设
如果在建表语句里只写int(10),不给它加任何额外属性,那这个 10 你在查询结果里根本感觉不到。比如你插入一个数字1,查询出来就是1,它不会自动显示成0000000001。所以可以说,在不使用 zerofill 的情况下,int(1)和int(10)从 SQL 执行结果上完全无法区分。
真正让 M 起作用的是zerofill属性,中文叫“零填充”。当你把字段定义成int(10) zerofill时,如果存储的数值位数不足 10 位,MySQL 在查询结果里会自动在左边补 0,补到 10 位为止。比如插入1,查询出来是0000000001;插入999,查询出来是0000000999。
这里有个容易忽略的细节:如果你定义的是int(1) zerofill,那插入1显示还是1,因为 1 本身就已经达到显示宽度了。宽度不足时不会补零,宽度足够时需要补零,这个逻辑和字符串填充非常像。因为int(1)的显示宽度只有 1,所以它几乎不会触发补零行为,除非你存负数——但下面马上要说,zerofill 字段其实存不了负数。
2.2 用了 zerofill 后会自动变成无符号
这是 zerofill 里第二个容易踩的坑。在 MySQL 里,一旦给整数列加上 zerofill 属性,该列会自动附带 unsigned 属性,也就是变成无符号整数。官方文档里明确写了“ZEROFILL implies UNSIGNED”,这不是可选行为,而是强制的。
什么意思呢?如果你建了money int(10) zerofill,那这个字段的取值范围就不再是-2147483648到2147483647,而是0到4294967295。你往里面插负数会直接报 Out of range,数据根本写不进去。很多人在做固定位数的编号、订单号时指望用 zerofill 补零,结果项目里又要存负数,两边需求一冲突才发现这个隐含约束。
从 MySQL 8.0.17 开始,zerofill 属性本身也已经被官方标记为废弃,和显示宽度一起进入淘汰流程。所以现在新项目里基本没有任何理由再去用 zerofill 做业务逻辑。它看起来像是一个很方便的展示工具,但实际会引入 unsigned 语义、版本兼容问题和跨库迁移风险,性价比很低。
2.3 生产环境别把显示宽度当业务约束
我见过一些项目把int(4)当成“这里最多只能填四位数”来用,这是非常危险的做法。因为显示宽度根本不产生任何输入限制,你往int(4)里插入123456完全没问题,它照样存进去,查询出来也照样是123456。真正要限制数字大小,要么用业务层校验,要么用更小范围的类型比如smallint,而不是指望括号里的数字。
同理,固定位数的业务编号、订单号这类需求,也不建议用int(M) zerofill去实现。原因有几个:第一,补零只是查询显示效果,导出的数据、通过客户端查看的数据可能不带补零,数据一致性不直观;第二,zerofill 隐含无符号,业务字段一旦需要负数就废了;第三,换数据库或者换版本后行为可能不一致,尤其 MySQL 8.0.19 之后显示宽度基本被忽略,你的int(10) zerofill在新版本里可能不再补零,至少 DDL 层面已经看不到那个 10 了。
更稳妥的方案是用 varchar 存储固定位数的编号,在业务代码里用LPAD或者字符串格式化统一补零;如果确实是纯数字且需要参与数值运算,那就用普通的 int 或 bigint,展示时再格式化。数据库层只负责存数据和保证数据完整性,展示格式的活应该交给应用层。
3. 为什么不看 int(1) 还是 int(10),要看 int 本身
3.1 4 字节的 int 到底能存多少
回到根子上,MySQL 的整数类型是按字节数划分的,不是按括号里的数字划分。TINYINT占 1 字节,SMALLINT占 2 字节,MEDIUMINT占 3 字节,INT占 4 字节,BIGINT占 8 字节。int(1)、int(10)、int都落在 INT 这一档,所以它们的容量天花板完全一致。
字节数和位数之间的关系其实挺简单:一个字节 8 位,intonation 4 个字节就是 32 位。有符号时最高位做符号位,能表示的最大值就是2^31 - 1,也就是 2147483647;最小值是-2^31,也就是 -2147483648。无符号时所有位都用来表示数值,最大值就是2^32 - 1,即 4294967295。只要理解了这一层,就不会再被括号里的数字带偏。
这里可以顺手记住一个常用判断:int有符号最大值是十位数 2147483647,所以如果你的业务主键、自增 ID 有可能超过这个数,比如几亿用户表、日志流水表,直接上bigint,不要心存侥幸。显示宽度救不了你,类型选错才是真正的大问题。
3.2 int(1) 和 tinyint(1) 是两码事
很多人还会把int(1)和tinyint(1)搞混,因为它们都带有(1),看起来好像差不多。实际上差别非常大:int(1)是 4 字节的 int 类型,取值范围是完整的 int 范围;tinyint(1)是 1 字节的 tinyint 类型,有符号范围只有-128到127。它们的共同点是显示宽度都写着 1,但底层数据类型完全不同。
tinyint(1) 在 MySQL 生态里还有一个特殊身份:它经常被当作布尔类型使用。MySQL 里的BOOL和BOOLEAN其实都不是独立的类型,而是TINYINT(1)的别名。你建表写flag BOOLEAN,执行SHOW CREATE TABLE看到的往往就是tinyint(1)。很多编程语言的驱动、ORM 框架在读取元数据时,也约定俗成地会把tinyint(1)映射成布尔值。
所以这里有一条实用建议:想存布尔状态,用tinyint(1)而不是int(1)。如果你用了int(1)存 0/1,功能上没问题,但白白浪费 3 个字节,而且一些 ORM 不会把它识别成布尔类型,代码里还要额外做转换,属于给自己找麻烦。等到 MySQL 8.0.19 之后,整数显示宽度被废弃,唯独tinyint(1)作为特殊情况被保留下来,也正是因为 MySQL 生态里有大量代码依赖tinyint(1)的布尔语义。
3.3 M 不影响索引、排序和比较
既然显示宽度只是展示属性,那它对数据库行为的影响面其实非常小。索引自然不用说了,int(1)和int(10)建出来的索引结构完全一样,B+ 树的比较逻辑完全按 int 数值来,不会因为显示宽度不同而走不同的路径。
排序也是同一个道理。很多人问“mysql 排序”时遇到数字排序乱掉的问题,通常是把数字存成了 varchar,导致排序按字符串字典序走,出现 1、10、2 这种结果。但如果字段是正儿八经的 int 类型,无论你写int(1)还是int(10),排序都严格按数值大小来,绝不会出现“1 排在 10 前面”这种字符串错觉。
WHERE 条件也一样。WHERE id_a = 1和WHERE id_b = 1执行计划、查询性能没有差异。退一步说,如果你真想让数据库层面的整数带上前导零去排序、去展示,那也得靠 zerofill 或者字符串类型,普通的int(1)帮不上任何忙。
4. MySQL 版本演进:8.0.17 之后这个问题会被历史淘汰
4.1 官方为什么要废弃显示宽度
看到这里你应该也感觉到了,int 显示宽度这个设计相当别扭。它既不能限制存储,又不参与运算,只在极少数配合 zerofill 的场景下影响查询结果,却成功误导了一大批开发者。MySQL 官方显然也意识到了这个问题,所以在 8.0.17 版本中正式将整数显示宽度标记为废弃,并在后续版本中逐步移除了对它的支持。
官方废弃的理由其实很直白:显示宽度本质上是客户端展示层的东西,不该出现在数据库字段定义里。它给开发者造成了一种虚假的安全感,让人觉得括号里的数字能控制输入长度,实际上根本没有约束力。再加上 zerofill 隐含 unsigned 这种隐藏语义,进一步增加了使用成本,整体设计得不偿失,废弃只是时间问题。
4.2 升级到 8.0.19+ 后建表行为的变化
从 MySQL 8.0.19 开始,整数类型的显示宽度正式不再支持,除了tinyint(1)这个布尔特例被保留,其他int(1)、int(10)、int(11)之类的写法在 DDL 解析后都会被忽略。你在建表语句里仍然可以写int(10)而不报错,但执行完再SHOW CREATE TABLE,看到的通常就只是int,不再有括号里的数字。
这个变化对存量数据没有任何影响,因为底层存储的始终是 4 字节 int,显示宽度从来不会改变数据。真正值得注意的是元数据层面的差异:如果你还在用 MySQL 5.7,脚本里SHOW CREATE TABLE会原样输出int(10);如果升级到 8.0.19+,同样一张表导出的 DDL 可能就变成int了。对于那些靠字符串比对 DDL 的巡检工具、表结构 diff 工具、数据迁移平台来说,这种输出差异需要提前适配。
4.3 老 DDL 和工具生成的脚本怎么办
如果你维护的是老项目,表结构里到处都是int(10)、int(11),不要慌,这些字段不需要重建表,数据也不会出问题。显示宽度被忽略之后,最直接的影响只是元数据展示变了,应用代码读到的数据库类型仍然是 int,索引、主键、外键逻辑一概不变。
真正需要动手检查的是自动化工具链。比如你用 pt-online-schema-change 做在线 DDL 变更,用 gh-ost 做无锁表结构迁移,或者用自研的比对系统去比对测试库和生产库结构,当两端 MySQL 版本不一致时,可能出现“明明逻辑相同,但字段定义字符串不同”的判定结果。建议在临时环境里做一次 5.7 到 8.0 的升级演练,把建表脚本重新导出,统一改造成不带显示宽度的写法,让工具链尽早适应新格式。
关于新项目的建表规范,我的态度很明确:直接写int,不要带括号。没有 zerofill 需求时,int(1)和int(10)都是画蛇添足;有显示需求时,也该用应用层格式化,而不是靠数据库的废弃特性。保持 DDL 干净,比保护一个莫名其妙的旧习惯重要得多。
5. 自己动手验证一遍:5 分钟消除所有疑虑
5.1 建表插入,看真实存储效果
说再多不如跑一遍。下面这组 SQL 在 MySQL 5.7 或 8.0.18 及更早版本上执行,能完整看到显示宽度和 zerofill 的效果;如果你用的是 8.0.19+,也可以执行,只是最后一节SHOW CREATE TABLE的输出里可能不再保留括号里的数字。
CREATE TABLE int_demo ( id_a INT(1), id_b INT(10), id_c INT(1) ZEROFILL, id_d INT(10) ZEROFILL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO int_demo VALUES (1, 1, 1, 1); INSERT INTO int_demo VALUES (999, 999, 999, 999); INSERT INTO int_demo VALUES (2147483647, 2147483647, 2147483647, 2147483647);注意第三行插入的数字是 int 有符号最大值 2147483647,四个列都能正常接收。如果你插入的是一亿以上的数据,id_c和id_d因为 zerofill 隐含 unsigned,也能接收更大的值,但id_a和id_b到了 4294967295 就会报 out of range,这个对比很能说明 unsigned 带来的边界差异。
5.2 查询结果对比一下
执行下面这条查询:
SELECT id_a, id_b, id_c, id_d FROM int_demo;在支持显示宽度的版本里,预期结果是这样的:
| 列名 | 类型定义 | 插入 1 显示 | 插入 999 显示 | 插入 2147483647 显示 |
|---|---|---|---|---|
| id_a | int(1) | 1 | 999 | 2147483647 |
| id_b | int(10) | 1 | 999 | 2147483647 |
| id_c | int(1) zerofill | 1 | 999 | 2147483647 |
| id_d | int(10) zerofill | 0000000001 | 0000000999 | 2147483647 |
可以看到,id_a虽然是int(1),但它照样显示 999 和 2147483647,完全没有长度限制。id_d因为带int(10) zerofill,在数值不足 10 位时自动补零,插入 1 显示为0000000001,插入 999 显示为0000000999。而当数值超过显示宽度时,比如 2147483647 本身是 10 位,已经达到int(10)的显示宽度,所以不再补零。
这里还有个细节值得顺手验证:id_c是int(1) zerofill,但插入 999 后显示仍然是999,MySQL 不会因为显示宽度不足就截断数据。它只会做补零,从来不做截断,这一点也和字符串类型的行为完全不同。
5.3 补零只是显示,存储和运算仍是数字
再用两条查询验证 zerofill 的“虚”的一面。第一条,用数字条件匹配:
SELECT id_a, id_b, id_d FROM int_demo WHERE id_d = 1;如果你插入了第一行(1, 1, 1, 1),这条查询能正常命中,说明虽然id_d查询显示为0000000001,但它在数据库里存的仍然是数值 1,比较时也按数值 1 处理,不会因为显示宽度变成字符串 “0000000001”。
第二条,做一次算术运算:
SELECT id_d, id_d + 0 AS numeric_val FROM int_demo WHERE id_a = 1;id_d + 0的结果是 1,而不是 0000000001。这从底层证明了补零只是查询结果的展示效果,存储、计算、比较全部走 int 数值逻辑。看完这几条 SQL,int(1)和int(10)的区别应该就不会再有任何疑问了。
6. 实战常见问题与面试速查
6.1 高频问题排查清单
| 问题 | 正确答案 | 容易踩的坑 |
|---|---|---|
| int(1) 是不是最多只能存 9? | 不是,int(1) 仍是完整 int 范围,能存到 21 亿以上 | 把显示宽度当成输入长度限制 |
| int(10) 是不是最多只能存 10 位? | 不是,有符号 int 上限 2147483647 就是 10 位,但这是类型决定,不是宽度决定 | 以为 10 是位数上限 |
| int(10) 和 int(11) 谁存得更多? | 一样多,都是 int,存储范围完全一致 | 以为 11 比 10 多一位容量 |
| 写 int(255) 可以吗? | 旧版本显示宽度上限是 255,可以但不代表能存 255 位 | 以为 255 是 255 位数 |
| int(10) unsigned 是什么意思? | 无符号 int,范围 0 到 4294967295,10 是显示宽度 | 忽略 unsigned 带来的范围变化 |
| 用了 zerofill 能存负数吗? | 不能,zerofill 隐含 unsigned,插入负数直接报错 | 拿 zerofill 做编号时突然存不进负数 |
这张表基本覆盖了我在社区里看到的高频误解。如果踩过其中任何一个,把第 5 节的实验 SQL 跑一遍,比看任何文档都管用。
6.2 面试这样回答才不翻车
面试里被问到int(1)和int(10)的区别,建议按三个层次回答,既展示基础扎实,又体现对版本演进的关注。
第一层,直接说结论:存储层面没区别,都是 4 字节 int,取值范围完全一致,索引、排序、性能也没有任何差异。第二层,解释括号里的数字是显示宽度 display width,主要配合 zerofill 使用,不加 zerofill 时没有任何实际效果。第三层,补充版本演进:从 MySQL 8.0.17 开始显示宽度被废弃,8.0.19 之后整数类型不再支持显示宽度,仅tinyint(1)因为布尔语义被保留。
如果面试官追问,为什么老建表语句里老看到int(11),就把我前面说的原因抛出来:int 有符号最小值是 -2147483648,算上负号共 11 个字符,所以显示宽度取 11 是为了完整显示最小值;无符号 int 最大值是 4294967295,共 10 位,所以很多工具导出int(10) unsigned。能说到这个层面,基本就是高分答案了。
6.3 我在实际项目里的体会
最后说点实在的。我最早写表结构时同样以为int(1)只能存一位数,后来在一场评审会上被 DBA 追问为什么总额字段要定义成int(1),当场翻官方文档才彻底搞明白。那次经历让我养成了一个习惯:所有 int 列建表时不写括号里的数字,让类型保持它本来的样子,需要 0/1 布尔就用 tinyint(1),需要更大范围就上 bigint,展示格式一律交给应用层。
这种习惯在 MySQL 8.0.19 之后尤其舒服,因为官方已经把显示宽度移除,写不写括号最终都会被忽略。与其守着旧习惯继续写int(10),不如现在就开始简化 DDL。再遇到同事问“int(1) 和 int(10) 哪个大”,把这篇里的 SQL 丢给他跑一遍,他可能比你先顿悟。