ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL int(1)与int(10)的真相:显示宽度不等于存储长度

MySQL int(1)与int(10)的真相:显示宽度不等于存储长度 从入门到进阶一次弄清 MySQL 的 int(1) 与 int(10)别再被网上的说法带偏了关于 MySQL 的int(1)和int(10)我几乎每隔一段时间就会在群里、论坛上看到有人提问是不是 int(10) 能存的数字比 int(1) 大int(10) 是不是占用更多磁盘空间甚至有人把括号里的数字当成了存储长度的上限直接在设计表结构时把手机号存成int(20)结果数据溢出报错。今天这篇就把这个经典误区一次性讲透涉及存储字节、取值范围、显示宽度、ZEROFILL、MySQL 8.0 语法变化等关键点。无论你是刚接触数据库的新手还是写了不少业务 SQL 的开发这篇文章都值得花五分钟认真看完尤其是后面几个验证步骤建议自己动手跑一遍。这个int(1) vs int(10)的问题本质上是把**显示宽度display width和存储长度storage length**两个概念搞混了。搞清楚这一点不仅能让你的建表语句更规范还能避免在迁移数据库或升级 MySQL 版本时踩到意想不到的坑。下面我从误区根源开始一层层拆开讲。1. 误区源头括号里的数字是怎么被传歪的先说一个现象很多人第一次接触 MySQL 的数据类型是在图形化工具里看别人建好的表。Navicat、phpMyAdmin 这类工具展示字段信息时会把int(11)、varchar(255)完整显示出来。于是大家自然会产生一个朴素的联想——varchar(255)括号里的 255 确实是最大字符数那int(11)括号里的 11 是不是就是最大位数呢这个联想有一定道理却也恰恰是误区最容易生根的地方。更麻烦的是网上大量早期教程和博客在讲int类型时用的句式往往是int(M)M 表示最大显示宽度取值范围是 1 到 255然后一笔带过显示宽度不影响存储。这句话本身没错但对新手来说它既没有解释清楚显示宽度到底是什么也没有给出直观的例子于是读者自然就按自己的理解——M 就是最大长度——去记忆了。等到做业务的时候看到int(1)就担心这列是不是只能存个位数看到int(10)就觉得这下稳妥了能存十位数其实两个字段的容量一模一样。1.1 真正该记的第一条结论int(1)和int(10)在存储层面没有任何区别。两条结论先放在这里后文全部围绕它们展开无论括号里写 1 还是 10int类型在 MySQL 中固定占用4 字节存储空间。无论括号里写 1 还是 10可存储的数值范围完全相同有符号时是-2147483648 到 2147483647无符号时是0 到 4294967295。括号里的数字是显示宽度display width它不影响存储不影响取值范围不影响索引效率只有在配合ZEROFILL属性时才影响查询结果的显示格式。这个知识点几乎每个 MySQL 面试题都会带上一笔但真正动手验证过的人并不多。2. 括号里的数字到底管什么显示宽度的底层机制既然括号里的数字不控制存储那它到底是怎么工作的这就得从 MySQL 的二进制协议和结果集元数据说起。MySQL 在返回查询结果时每个字段除了携带值本身还会携带一份元数据其中包括字段类型、字符集、以及显示宽度。客户端比如命令行 mysql、JDBC 驱动、各种图形化工具拿到这份元数据后可以用来决定如何对齐、渲染结果。你可以把显示宽度理解成一个格式化提示它告诉客户端这个字段理想的展示宽度是多少位。但请注意它仅仅是提示。客户端是否遵守这个提示完全取决于客户端自己的实现。比如MySQL 官方的命令行客户端mysql在普通查询时会用显示宽度来对齐列。JDBC 的ResultSetMetaData.getPrecision()在某些版本中会返回显示宽度某些版本返回的是真正的精度信息。Navicat、DBeaver 这类 GUI 工具绝大多数情况下根本不理会显示宽度它们按照实际数据长度渲染。所以你会发现一个很微妙的现实显示宽度这个东西在没有ZEROFILL的情况下对绝大多数现代客户端来说几乎等于不存在。2.1 ZEROFILL让显示宽度真正生效的唯一场景如果建表语句里给某个整型字段加了ZEROFILL属性MySQL 的行为就会发生实际变化。看一个最直观的例子CREATE TABLE demo_int ( a INT(1) ZEROFILL, b INT(10) ZEROFILL ); INSERT INTO demo_int (a, b) VALUES (5, 5); SELECT * FROM demo_int;执行结果------------------- | a | b | ------------------- | 00005 | 0000000005 | -------------------这里能看到关键差异a是INT(1) ZEROFILL值 5 被显示为00005等等这里容易产生第二个误解——INT(1)加上 ZEROFILL 后为什么不是只显示 1 位原因在于如果实际值的位数超过了显示宽度MySQL 会完整显示实际值而不是截断。同时ZEROFILL还有一个隐藏行为——一旦启用该列会自动转为无符号UNSIGNED。这是很多人写建表语句时被坑过的地方明明没写UNSIGNED结果字段变成无符号了负数插不进去。回到显示宽度本身。当数字位数不足显示宽度时MySQL 会在数字左侧补 0直到达到显示宽度指定的位数。INT(1)的显示宽度是 1但值 5 已经达到 1 位按理说不需要补 0——那为什么上面显示的是00005而不是5这里就引入了 MySQL 的另一个规则显示宽度的最小值实际是 2或者说对于 ZEROFILL 列MySQL 内部会做一个处理。准确来说在 MySQL 5.x 和 8.0 中如果你写INT(1) ZEROFILLMySQL 会自动把显示宽度调整为至少容纳该类型无符号最大值所需的位数。也就是说INT(1) ZEROFILL实际生效的显示宽度会被提升到最大值的位数——对于INT无符号最大值是 4294967295共 10 位所以INT(1) ZEROFILL实际按 10 位显示值 5 就变成了0000000005。这个自动调整的行为在不同版本里略有差异但结论是一致的你没法通过把显示宽度写小来限制ZEROFILL 的补零位数它一定要能容纳当前类型的最大值。2.2 数值超过显示宽度时怎么办再验证一个问题如果存进去的数字超过了显示宽度会不会被截断、报错INSERT INTO demo_int (a, b) VALUES (12345678901, 12345678901);对于无符号INT来说12345678901 已经超过了 4294967295 的上限这条插入会直接报Out of range value错误。换一个在范围内但超过显示宽度的值INSERT INTO demo_int (a, b) VALUES (123456789, 123456789); SELECT * FROM demo_int;结果会原样显示123456789不会被截断。也就是说显示宽度只负责不够位时补零永远不会截断超宽的值。这一点和varchar(n)的行为有本质区别——varchar存超长字符串时会按照 sql_mode 决定是报错还是截断而整型的显示宽度压根不参与数据校验。3. 存储和运算的真相int 的大小其实是固定的这一节我们从更底层的视角把整型在 MySQL 中的存储机制讲透。很多人被括号里的数字干扰忘记了数据类型本身的含义。INT是 MySQL 的一种数据类型它决定了存储字节数、取值范围、参与运算的方式。显示宽度只是数据类型之外的附属属性属于外观层面的修饰。3.1 字节数、取值范围和位数对照MySQL 的整数类型共有TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT五种它们的差异体现在存储字节数和取值范围上。INT固定占 4 字节也就是 32 位。这个 32 位是硬件层面的和括号里的显示宽度没有任何关系。类型存储字节有符号范围无符号范围TINYINT1-128 ~ 1270 ~ 255SMALLINT2-32768 ~ 327670 ~ 65535MEDIUMINT3-8388608 ~ 83886070 ~ 16777215INT4-2147483648 ~ 21474836470 ~ 4294967295BIGINT8-9223372036854775808 ~ 92233720368547758070 ~ 18446744073709551615你可以看到INT(1)和INT(10)都落在INT这一行存储字节都是 4取值范围都是同一组数字。计算机底层用 32 位二进制来表示这个数无论如何也不会因为你想让它在界面里显示得窄一点或宽一点而改变存储布局。3.2 索引性能和存储空间同样的 4 字节没有捷径基于上面的结论int(1)和int(10)建索引时创建的 B-Tree 索引在存储和性能上也没有任何区别——因为它们底层存的就是同一种 4 字节整型。索引键值长度由数据类型决定与显示宽度无关。这带来一个实际建议不要试图通过把int的括号数字改小来压缩表空间或提升索引性能这是无效操作。真正能减少整型存储空间的思路是选择合适的数据类型状态值用TINYINT省内编码用SMALLINT中大规模的自增主键用INT数据量上亿且需要超大主键时才考虑BIGINT。搞清楚存储字节数比纠结括号里写几更有意义。3.3 运算时会发生什么再来一个容易踩的坑int(1)列和int(10)列做 JOIN 或 UNION 时会不会因为宽度不同而类型不匹配答案是不会。MySQL 在判断两个整数类型是否兼容时看的是INT这个基础类型而不是显示宽度。INT(1)和INT(10)在类型系统里被认为是完全相同的类型可以无缝 JOIN、UNION、比较。这一点从information_schema.COLUMNS表也能看出来INT(1)和INT(10)的DATA_TYPE字段都是int只有COLUMN_TYPE字段才会带上显示宽度的文本。4. 实操验证一条 SQL 看清显示宽度的真实面貌光说不练假把式。这一节我们跑一套完整的验证 SQL把上面的结论落在地上。如果你手边有 MySQL 环境直接复制执行即可没有环境的话可以先用SELECT语句模拟不需要真的建表。4.1 用 DDL 验证元数据CREATE TABLE int_test ( c1 INT(1), c2 INT(10), c3 INT(1) ZEROFILL, c4 INT(10) ZEROFILL );建表成功后查询元数据SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE FROM information_schema.COLUMNS WHERE TABLE_NAME int_test;结果大致如下COLUMN_NAMEDATA_TYPECOLUMN_TYPECHARACTER_MAXIMUM_LENGTHNUMERIC_PRECISIONc1intint(1)NULL10c2intint(10)NULL10c3intint(1) unsigned zerofillNULL10c4intint(10) unsigned zerofillNULL10这里有两个信息值得注意NUMERIC_PRECISION都是 10表示十进制精度的位数它与括号里的显示宽度无关而是由INT类型本身的数值范围决定的最大 10 位十进制数。c3列因为加了ZEROFILL被自动加上了unsigned属性c4同理。4.2 用查询验证 ZEROFILL 的补零行为INSERT INTO int_test (c1, c2, c3, c4) VALUES (5, 5, 5, 5); SELECT c1, c2, c3, c4 FROM int_test;结果------------------------------------ | c1 | c2 | c3 | c4 | ------------------------------------ | 5 | 5 | 0000000005 | 0000000005 | ------------------------------------这组结果完美展示了显示宽度只在 ZEROFILL 时生效以及宽度不足时按类型最大值位数补零的规则。你可以看到c3和c4的输出完全一样都是 10 位补零说明INT(1) ZEROFILL并没有缩窄成 1 位的显示。也许有读者会问为什么我不说INT(1) ZEROFILL会保持 1 位、补成 1 位因为 MySQL 文档和实际行为都表明ZEROFILL 列的实际显示宽度至少等于该类型无符号最大值的长度。如果你确实想让一个整型以 3 位方式补零显示比如007那你应该用INT(3) ZEROFILL并且要清楚存 100 时它会显示100存 1000 时显示1000不会被截断。4.3 验证取值范围不受显示宽度影响INSERT INTO int_test (c1, c2) VALUES (2147483647, -2147483648); SELECT c1, c2 FROM int_test;两条记录都能正常插入和查询。再把2147483648插进去INSERT INTO int_test (c1, c2) VALUES (2147483648, 0);会立刻报Out of range value for column c1错误。注意报错与c1是int(1)无关——就算你把c1改成int(10)2147483648 照样超范围。真正能解决这个插入需求的方案是改用INT UNSIGNED或BIGINT。5. 版本演进与迁移注意MySQL 8.0 后的显示宽度如果你是从 MySQL 5.7 或更早版本升级到 MySQL 8.0 的这里有一个非常值得留意的变化很多人升级完才发现建表语句和SHOW CREATE TABLE的输出不一样了。5.1 MySQL 8.0 去掉了整型显示宽度语法从 MySQL 8.0.17 开始官方移除了为整数类型指定显示宽度的功能。严格来说是不再允许在INT、TINYINT、SMALLINT、MEDIUMINT、BIGINT的声明中指定显示宽度只有TINYINT(1)等仍然会被接受但不推荐更重要地MySQL 8.0 在SHOW CREATE TABLE的输出中不再包含int(10)这种语法。举个例子在 5.7 中CREATE TABLE t (id INT(10) NOT NULL); SHOW CREATE TABLE t;会输出CREATE TABLE t ( id int(10) NOT NULL ) ENGINEInnoDB ...同样在 8.0.19 中执行CREATE TABLE t (id INT(10) NOT NULL); SHOW CREATE TABLE t;输出变成了CREATE TABLE t ( id int NOT NULL ) ENGINEInnoDB ...int(10)这个声明虽然还能解析不会报语法错误但元数据层面已经不再保留显示宽度了。如果你有工具在解析COLUMN_TYPE字段做代码生成或数据字典比对升级到 8.0 后COLUMN_TYPE会从int(10)变成int这可能导致你构建的 ORM 元数据缓存、数据库 diff 工具、监控平台出现字段类型变化的误报。还有一个反向问题MySQL 8.0 中从 5.7 迁移过来的表原本是int(10)升级之后SHOW CREATE TABLE不再显示(10)但数据本身完全不受影响。所以如果你手上有自动化脚本靠COLUMN_TYPE判断字段是否是INT建议改成判断DATA_TYPE int。5.2 ZEROFILL 仍在但显示宽度被固定化MySQL 8.0 虽然移除了整型显示宽度语法但ZEROFILL 属性仍然保留。也就是说INT ZEROFILL、INT(5) ZEROFILL这类写法仍然兼容只是(5)在元数据里不再被当作独立属性存储。实际效果依旧是值不够 5 位时补零超过 5 位时原样显示并且列自动变为UNSIGNED。如果你依赖 ZEROFILL 实现类似流水号的展示效果8.0 里依然可行。但请注意ZEROFILL 补零只是显示行为存储的仍然是整数。如果你在应用层用它来做字符串拼接一定要记得先转字符串再处理不能依赖数据库返回的格式化文本。5.3 迁移到 8.0 前的检查清单根据我处理过的迁移案例升级 MySQL 8.0 前建议做好这几件事扫描所有建表语句把int(N)批量替换为int除非你明确需要 ZEROFILL 的补零展示。检查数据字典比对脚本确认没有依赖COLUMN_TYPE的字符串匹配逻辑。检查 ORM 框架的字段元数据缓存比如 Hibernate 的table结构校验避免升级后报字段类型不一致。如果确实需要 ZEROFILL保留INT(N) ZEROFILL写法但要知道 N 的影响在 8.0 中已趋于弱化建议直接用INT ZEROFILL。有读者可能会问那 8.0 里TINYINT(1)的语义是不是也被改了这个要说一下TINYINT(1)在 MySQL 中长期以来被很多 ORM比如 Hibernate 的boolean映射当作布尔值使用因为 Java 端的Boolean映射到 MySQL 常常生成bit或tinyint(1)。8.0 移除显示宽度后tinyint(1)在元数据里会变成tinyint或tinyint(1)取决于具体版本和工具但底层存储和取值仍然是一个字节。如果你用tinyint(1)存布尔值升级到 8.0 后没有实际风险只是你没法再靠(1)这个记号告诉别人这列是布尔语义了——建议在注释里写明或者干脆用BOOLEAN/BOOL类型它们在 MySQL 里本质是TINYINT(1)的别名。6. 避坑指南实际业务里不要再这样用 int 了最后这部分我给几个从真实项目里总结出来的经验。这些坑不一定每个人都踩过但一旦踩到排错成本往往不低。6.1 别把手机号、身份证号设计成 int这是最经典也最无语的一个坑。浏览器里输入mysql int 手机号搜索结果里大概率能看到有人建议手机号用bigint或varchar(20)存储。为什么不能放int里因为INT无论你写int(11)还是int(10)最大值都是 2147483647而 11 位手机号前两位基本都是1开头13800138000 明显超过 21 亿。存进去要么报错要么被截断成负数。正确的做法是手机号VARCHAR(20)或CHAR(11)因为手机号不应该参与数学运算且可能包含国家码、扩展位。身份证号CHAR(18)原因同上还需要考虑 X 结尾。银行卡号VARCHAR(32)。有人可能会反驳那我把手机号存BIGINT不就行了理论上能存但你不该把编号设计成数字类型——一来没必要做加法二来前导零场景某些业务编号如订单号可能前缀固定会导致展示丢 0三来 Java 的 long 和 JavaScript 的 Number 之间还有精度问题。能用字符串表示的业务编号就别用整型。6.2 别用显示宽度来限制输入曾经有个项目业务方要求用户 ID 必须是 6 位设计者就把字段定义成了int(6)以为这样输入超过 6 位就会被 MySQL 拒绝。实际结果MySQL 完全不校验位数1234567 照常插入前端和后端也没有做校验最后程序在展示页面上炸了因为页面代码假定 ID 一定是 6 位拼接成000001这种格式去查接口。教训是位数限制是业务规则应该在应用层校验或者用CHECK约束、触发器实现而不是指望整型显示宽度拦截。如果你确实要严格限制 6 位正确的做法是用VARCHAR(6)加正则校验或者用DECIMAL(6,0)配合CHECK(LENGTH(id) 6)当然这也不是推荐方案——主键用字符串是另一场灾难。6.3 ZEROFILL 会自动转 UNSIGNED小心负数这一点前文提过但因为太容易踩我单独拎出来说。看这个操作CREATE TABLE t ( score INT(4) ZEROFILL ); INSERT INTO t (score) VALUES (-10);你以为能插入-0010实际上数据库会报错因为score已经是无符号的不允许负值。如果你要给可空列加ZEROFILL还要考虑NULL值的显示——NULL不会被补零它依然显示为NULL而0会被补成0000。这在报表统计时是个经典迷惑点COUNT(*)统计正常但肉眼扫数据时看到一堆0000容易误以为数据被污染了。6.4 建表工具生成的 int(11) 不要当成参考规范到处抄很多 ORM 自动建表的工具比如旧版 Hibernate、MyBatis Generator、Liquibase 的某些模板生成的主键字段默认都是int(11)。这其实是历史遗留惯例因为INT有符号时最大 2147483647正好 10 位加上正负号共 11 个字符位置所以显示宽度取 11 最美观。int(10)常见于无符号场景10 位足够覆盖 4294967295。实践中我不建议在新代码里纠结int(10)还是int(11)直接写int或int unsigned即可。如果你在维护老系统也不要因为SHOW CREATE TABLE显示int(10)就认为它和另一个库里的int(11)不兼容——它们是同一个东西不同的显示宽度而已。6.5 别把显示宽度和字符集长度混为一谈最后再强调一个容易混淆的点varchar(255)里的 255 是最大字符数它会限制输入长度而int(10)里的 10 只是显示宽度不限制输入长度。两者的语义完全不同。处理两个类型时心智模型应当是字符串类型括号里的数字是业务约束的一部分直接影响存储和校验。整数类型括号里的数字只是客户端渲染提示存储和校验完全不受它影响。所以当你看到别人写的varchar(100)时可以合理推断这个字段最多存 100 个字符而看到int(100)时心里要明白这不过是个显示宽度的无害摆设实际还是 4 字节的 INT。写在最后关于 int 类型我的两条实操建议这段不算总结就算是一个数据库开发老兵的个人体会吧。第一新项目建表时所有整型字段一律不要写显示宽度直接INT、BIGINT、INT UNSIGNED。这样代码更干净也避免了迁移 MySQL 8.0 时被COLUMN_TYPE的差异干扰。唯一例外是TINYINT(1)——如果你在用老版本 ORM 映射布尔值保留它能让工具识别得更顺畅但升级到 8.0 后也建议逐步替换为BOOLEAN或带注释的TINYINT。第二如果你真的需要定长补零的展示效果比如订单号000123不要依赖 ZEROFILL我建议直接在应用层用字符串拼接或者在后端计算时补零。原因有三ZEROFILL 会强制 UNSIGNED限制插入负值它对NULL不生效以及 MySQL 8.0 已经在弱化整个显示宽度机制长期依赖不是好主意。数据库层面只存干净的整数展示格式化交给业务层各司其职这才是最不容易出问题的做法。按这个思路理顺之后你再看网上的int(1)和int(10)之争基本一眼就能判断对方说的靠不靠谱。记住核心的三句话INT固定 4 字节取值范围看有符号还是无符号括号数字只管显示且只在 ZEROFILL 时有效。这三句话能帮你省下很多无谓的排查时间。
返回列表