news 2026/10/2 9:31:28

MySQL整数类型选型:TINYINT/INT/BIGINT存储原理与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL整数类型选型:TINYINT/INT/BIGINT存储原理与避坑指南

MySQL 的整数类型看起来简单,TINYINT、INT、BIGINT 在大多数人眼里无非就是“能存多大的数”的区别。但我在一线帮人排查线上问题时发现,类型选错造成的故障,往往比 SQL 写错更隐蔽、更致命。上个月朋友公司的一张 6000 万行流水表,就因为主键用了 INT 差点写不进去,全组 DBA 加班到凌晨。今天我把三种整数类型的存储原理、选型方法和踩坑经验一次性讲透。

1. 先讲个真实翻车案例:6000 万行的流水表,INT 主键差点扛不住

朋友公司的核心业务是一张流水表,主键id INT NOT NULL AUTO_INCREMENT,跑了一年半,快接近 21 亿的 INT 上限了。他们老板还在谈新合作,说数据量再翻一倍没问题,结果某天凌晨告警:主键继续自增的插入开始出现Duplicate entry报错——虽然行数还不到 21 亿,但自增计数已经越过边界,再插入就冲突了。

很多人觉得 21 亿很大了,毕竟地球人也就 80 亿。但在互联网业务里,流水表、日志表、埋点表是增长速度最吓人的三类表。尤其当你用AUTO_INCREMENT做单机自增主键时,INT 的 4 字节空间会被快速消耗。

这个案例让我意识到一件事:整数类型选型,本质是在做数据规模的预判。你不需要精确知道未来有多少行,但你至少要判断它会不会超过 21 亿、能不能控制在 42 亿以内(无符号 INT)、或者干脆直接用 BIGINT 一劳永逸。

2. 1 字节、4 字节、8 字节:TINYINT/INT/BIGINT 的存储魔力和数学本质

先看基础数据,这是整篇文章的地基:

类型字节数位数有符号范围无符号范围
TINYINT18-128 ~ 1270 ~ 255
SMALLINT216-32768 ~ 327670 ~ 65535
MEDIUMINT324-8388608 ~ 83886070 ~ 16777215
INT432-2147483648 ~ 21474836470 ~ 4294967295
BIGINT864-9223372036854775808 ~ 92233720368547758070 ~ 18446744073709551615

如果你记不住,就记住一条计算规律:n 位二进制有符号整数的最大值是 2^(n-1) - 1,最小值是 -2^(n-1);无符号最大值是 2^n - 1。

比如 INT 是 32 位,有符号最大就是 2^31 - 1 = 2147483647。很多人在建表时对“大概够用”产生错觉,问题就出在没把指数增长放在心上:2 的 31 次方看起来是 21 亿,但把一张表的每一行自增主键换算成“每天写入量 × 天数”,你很快会算出临界时间。

2.1 存储字节背后影响的不是“一个数字”,而是整棵索引树

MySQL 默认的 InnoDB 引擎,B+ 树索引页的大小固定为 16KB。这意味着:

  • 主键越短,单个索引页能容纳的键值越多,树的高度越低;
  • 二级索引的叶子节点会存储主键值,所以主键越大,所有二级索引占用的空间也会水涨船高。

举个例子:一张 10 亿行的表,如果主键是 BIGINT(8 字节),比 INT(4 字节)在单个二级索引上可能多出 4GB 以上的存储空间。为了给 20 年后可能出现的“超大数”留余地,你把全表所有索引的体积扩大,换来的是缓冲池命中率下降、磁盘 IO 上升。这也是我为什么特别反对“无脑 BIGINT”。

2.2 别用浮点数或字符串存整数,这是两笔智商税

我见过有人用DOUBLE存金额、用VARCHAR(20)存订单号。整数用浮点存,比较时会产生精度误差;用字符串存,排序会变成字典序,10会排在9前面。TINYINT/INT/BIGINT 用二进制存储,比较和计算都是 CPU 原生运算,这是数据库设计里最不该妥协的部分。

3. 结合业务场景定类型:状态值、业务ID、时间戳、自增主键怎么选

很多人的建表习惯是:打开 Navicat,字段类型统一INT(11),不够就 BIGINT。这种操作方式省事,但会给后续埋雷。我把常见场景整理成一个选型表,建议收藏:

数据特征推荐类型理由
布尔值/枚举状态(如 0、1、2)TINYINT1 字节,256 个取值足够
小范围业务编码(如分类 ID)SMALLINT 或 MEDIUMINT省空间,范围可预期
普通计数器、文章浏览数INT单表百万级时足够,21 亿很难突破
自增主键(日增百万级别)BIGINT 或 INT UNSIGNED提前预留增长空间
秒级时间戳INT UNSIGNED 或 BIGINTINT 有 2038 年问题,建议直接 BIGINT
雪花 ID / 分布式 IDBIGINT几十位的整数,只有 BIGINT 装得下
金额(单位:分)BIGINT 或 DECIMAL整数存分能避免浮点误差

3.1 TINYINT 不是“省一点空间”,而是把状态控制写进了数据库

状态字段用 TINYINT,其实是在告诉团队:这个字段只允许容纳个小集合。配合CHECK约束或者枚举约束,乱写状态值的概率会小很多。我看过不少表演示代码,用INT存一个只有三种状态的status,浪费 3 个字节还是小事,关键是将来你不知道它会不会被塞进一个奇怪的值。

3.2 时间戳为什么别用 INT 了

很多老系统用INT UNSIGNED存 Unix 时间戳,上限 42 亿秒,到 2106 年才溢出,看起来没问题。但如果是有符号 INT,2038 年就会溢出,这就是著名的 Y2038 问题。其实问题本质是:32 位有符号整数只装得下 1970 到 2038 年的秒数。

我的建议是:新表时间字段要么用 DATETIME,要么用 BIGINT 存毫秒级时间戳,别再用 INT 了。分布式系统里时间戳经常是毫秒精度,一个毫秒级时间戳已经超过 17 亿,秒级计算的话 32 位还能凑合,毫秒级必须上 BIGINT。

3.3 一个典型的建表选型案例

CREATE TABLE user_activity_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID,来自分布式ID', activity_type TINYINT NOT NULL DEFAULT 0 COMMENT '活动类型:0=点击,1=收藏,2=下单', duration_ms INT UNSIGNED NOT NULL COMMENT '停留时长(毫秒)', log_date DATE NOT NULL, PRIMARY KEY (id), KEY idx_user_date (user_id, log_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为日志表';

这里每一个BIGINT都不是随便选的,而是对应上游系统生成的 64 位 ID。你把user_id换成 INT,当时建表没问题,等上线联调发现 ID 溢出,就晚了。

4. INT(11) 和 ZEROFILL:早该被遗忘的显示宽度,为什么还有人在问

你在老版本的 Navicat 或 mysqldump 导出脚本里,经常看到int(11)、tinyint(4)、bigint(20)这种写法。括号里的数字叫显示宽度(display width),它和存储空间、取值范围毫无关系,纯粹是告诉 MySQL“当展示数字时最少补几个零”。这是一个历史遗留特性,很容易让新手误以为INT(4)比INT(11)存得少,实际上两者能存的范围一模一样。

4.1 显示宽度最迷惑人的地方:ZEROFILL

如果把字段定义为:

CREATE TABLE t_demo ( num INT(5) ZEROFILL );

插入123后查询,你会看到00123。ZEROFILL 会在数字左侧补零,而它依赖显示宽度。这个特性看起来很酷,实际业务里几乎没人用,因为它把“数字”变成了“带样式的文本”,还会影响程序读取时的类型判断。

4.2 MySQL 8.0 的态度:废弃它

从 MySQL 8.0 开始,整数类型的显示宽度基本已被废弃,新导出工具生成的建表语句已经不再包含INT(11)这种写法了。如果你还在用旧习惯写INT(11),代码能跑,但没必要。我更推荐建表时直接写INT或INT UNSIGNED,不带括号,干净利落。

提示:如果你维护的是老项目,导出 SQL 里全是int(10) unsigned,不要试图把括号里的数字删掉再导入,它们对现有数据没有任何影响,删了反而容易引起不必要的 diff。

5. UNSIGNED 不是银弹:无符号整数的三个隐藏雷区

无符号整数把负数范围让给了正数,所以 INT UNSIGNED 上限是 42 亿而不是 21 亿。听起来很好,但我在实际项目里吃过三次亏。

5.1 雷区一:主键一旦溢出,回退空间变小

如果主键用INT UNSIGNED,你会觉得“42 亿够大了”。但当它真的满的那一天,你要面临的不是“改 BIGINT 就行”,而是整整一个生产库的索引重建。反过来,如果一开始用有符号 INT,在接近 21 亿时你还有“先改成 UNSIGNED 顶一顶”的空间。虽然我不推荐拿 UNSIGNED 当续命手段,但它确实是排查问题时的额外退路。

5.2 雷区二:应用语言可能不支持无符号类型

Java 里的int最大只能表示 21 亿,你从数据库查出INT UNSIGNED的值是 30 亿时,用int直接接收会变成负数。很多 ORM 框架默认把 INT 映射成 Java Integer,一旦遇到无符号 INT 的大值,就会踩坑。这属于典型的“数据库类型和语言类型不匹配”。

5.3 雷区三:混合运算时隐式转换很诡异

看这段 SQL:

SELECT CAST(-1 AS SIGNED) = CAST(1 AS BIGINT UNSIGNED);

如果你拿有符号数和无符号数做比较,MySQL 会把有符号数转成无符号数,-1会变成一个巨大的正数,比较结果完全违背直觉。这类 bug 的排查难度极高,因为 EXPLAIN 不会直接报错,只有执行结果诡异。

所以我的经验是:除非你非常确定字段永远不会参与负数运算、也不会和其他有符号字段关联,否则不要随手加 UNSIGNED。主键可以例外,但也要评估应用层的读取逻辑。

6. 关联字段类型不一致:索引失效、慢查询和数据错乱的真正源头

整数类型选型还有一个特别隐蔽的坑:两个表关联时,字段类型必须完全一致。我把这个放在最后单独说,因为它和 TINYINT/INT/BIGINT 的关系太密切了。

6.1 典型的关联字段类型错配

假设users表的id是 BIGINT,而orders表的user_id是 VARCHAR(20)。你写:

SELECT * FROM orders o LEFT JOIN users u ON o.user_id = u.id;

执行计划里,MySQL 大概率会对orders做全表扫描。因为o.user_id是字符串,u.id是整数,MySQL 会把字符串隐式转换成数字再比较,这意味着索引列上发生了函数操作,索引自然失效。

6.2 更隐蔽的同类问题:INT 和 BIGINT 关联

有人问:两个字段都是整数,一个 INT,一个 BIGINT,应该没问题吧?问题恰恰出在这里。如果orders.user_id是 INT,users.id是 BIGINT,关联时一样可能发生隐式转换,导致索引无法有效利用。因为 MySQL 在比较不同类型的整数时,会把低精度类型向高精度类型转换,这在执行计划里会被视为对列做了类型转换。

6.3 排查方法

遇到慢查询,先跑一遍:

EXPLAIN SELECT ...

看type列,如果出现ALL,且Extra列有Using where字样,就要怀疑关联字段类型不一致。再用:

SHOW CREATE TABLE orders; SHOW CREATE TABLE users;

逐个核对关联字段的列类型、有无符号、长度,三者必须一致。

注意:不仅是关联字段要一致,WHERE条件里的字段也要一致。比如WHERE user_id = 12345678901,如果user_id是 BIGINT,字符串常量会被转成数字,这是安全的;但如果字段是 VARCHAR,常量是数字,索引也会失效。

7. 选型错误之后怎么办:ALTER TABLE 的代价和应急方案

如果真的发生了 INT 主键临近溢出,怎么办?我的处理步骤是:

  1. 先确认当前自增值:
SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'your_table';
  1. 低峰期执行类型变更:
ALTER TABLE `your_table` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT;

这条 DDL 在 MySQL 8.0 里可以用在线方式执行,但表很大时依然要重建索引,耗时可能以小时计。我见过 2TB 的表改主键类型,跑了整整一个晚上,期间从库延迟一度拉到 10 分钟以上。

  1. 如果业务完全不能停,那就走双写方案:新建一张 BIGINT 主键的新表,从旧表导入数据,同时业务层把新写入切到新表,最终割接。这是最稳但最费劲的方案。

我个人现在的原则是:能选 BIGINT 的场景,我不会省这 4 个字节;但不该用 BIGINT 的小状态字段,我也绝不会去浪费。整数选型从来不是数学题,而是对业务增长曲线的预判题。希望这篇帖子能让你在建表时多想一步,少熬一个夜。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 9:31:20

Vivado常见错误诊断与工程级排错指南

1. 这不是报错清单,而是一份Vivado工程师的“故障诊断手记” 你刚在Vivado里点下“Generate Bitstream”,进度条走到87%突然卡住,控制台刷出一长串红色文字——不是语法错误,不是时序违例,而是类似 ERROR: [Synth 8-4…

作者头像 李华
网站建设 2026/10/2 9:31:19

LLM工程落地:CUDA、Transformer与AI Agent硬核实战指南

1. 这不是“又一个LLM教程”,而是一份面向真实工程落地的系统性认知地图你点开这个标题,大概率正站在AI学习的十字路口:一边是铺天盖地的“30分钟入门大模型”“手撕Transformer”短视频,一边是打开Hugging Face文档时满屏的forwa…

作者头像 李华
网站建设 2026/10/2 9:31:19

KingbaseES PLSQL异常处理实战:捕获、事务回滚与批量优化

跑批凌晨突然短信告警,一张大表的存储过程执行到一半卡死;开发环境里明明好好的,生产上却抛了ORA-01403;加了异常处理之后,业务反而更慢了……这些场景你是否熟悉?我在不少基于KingbaseES的迁移和运维项目里…

作者头像 李华
网站建设 2026/10/2 9:31:19

Docker容器化实战:从安装部署到MySQL、Redis与微服务编排

1. Docker到底解决了什么问题 先用大白话把核心讲清楚:Docker是一款开源的容器化平台,它把你的应用连同运行环境一起打包成一个标准化的“镜像”,然后通过“容器”这个隔离环境跑起来。以前你最头疼的“在我电脑上明明能跑,怎么换…

作者头像 李华
网站建设 2026/10/2 9:31:18

MCP协议与LangGraph实战:构建高可靠AI Agent系统

1. 这不是又一个“AI Agent速成班”,而是一份能让你在真实项目里写得出、跑得通、扛得住压的实战手记你点开这个标题,大概率不是为了听“Agent是智能体”“LangChain是编排框架”这种教科书定义。你真正想问的是:我昨天刚用LangChain搭了个天…

作者头像 李华
网站建设 2026/10/2 9:30:12

NVIDIA AI芯片深度解析:从GPU并行计算到CUDA生态与部署实战

NVIDIA这几个字母,这几年几乎成了AI的代名词。从大模型的预训练到推理部署,从自动驾驶到生命科学,你很难找到一个完全不用NVIDIA芯片的严肃AI项目。我身边的工程师朋友们聚会,聊着聊着总会绕回同一个话题:这家公司的AI…

作者头像 李华