自增id能减少页分裂,因为其递增特性使插入总在索引页尾部追加,页满时仅需分配新页并链入;而随机主键需定位中间页插入,易触发分裂,导致数据迁移、指针更新及多层节点调整。

MySQL InnoDB 表用非递增主键,插入性能会明显下降,根本原因是页分裂频繁 —— 这不是配置问题,是 B+ 树索引的物理结构决定的。
为什么自增 ID 能减少页分裂
InnoDB 的主键索引就是聚簇索引,数据直接按主键顺序存放在 16KB 的数据页里。当新行插入时,如果 id 是递增的,InnoDB 总是追加到当前最后一个叶子页末尾;页满时只需分配一个新页,链表尾部接上即可。
但如果主键是随机值(比如 UUID、MD5、业务编码),InnoDB 得先定位到中间某个页,再尝试插入。一旦该页已满,就必须分裂:把约一半数据挪到新页,更新父节点指针,还可能触发多层节点调整。
- 页分裂不是原子操作,会产生额外 I/O 和锁等待
- 分裂后页利用率下降(常低于 50%),后续
OPTIMIZE TABLE成为刚需 - 碎片多了,缓冲池(
innodb_buffer_pool)缓存效率也会变差
哪些主键实际会引发严重页分裂
以下场景在真实业务中高频出现,但容易被误认为“只是主键长得不一样”:
-
CHAR(32)或VARCHAR(36)类型的UUID(尤其是未做REVERSE()或UUID_TO_BIN()优化的) - 由多个字段拼接生成的业务主键,如
CONCAT(DATE_FORMAT(NOW(),'%Y%m%d'), LPAD(id,6,'0')),时间部分虽有序,但id段不保证全局唯一递增 - 使用
SELECT MAX(id)+1手动算 ID 并INSERT—— 在并发下极易重复或跳号,且无法避免中间插入 - 分库分表后各子表仍用本地
AUTO_INCREMENT,导致全局主键无序(如订单号在不同库中都是 1,2,3…)
MySQL 8.0 后的自增计数器持久化影响
MySQL 8.0 开始,AUTO_INCREMENT 计数器值写入 redo log 并在 checkpoint 持久化,重启后不会回退到 MAX(id)+1。这看似更可靠,但也带来两个易忽略点:
- 如果你曾手动
INSERT INTO t(id) VALUES(1000),之后即使删掉该行,计数器也不会自动回退 —— 下次自增仍从 1001 起 -
innodb_autoinc_lock_mode=2(默认)下,简单插入(INSERT ... VALUES)只在申请 ID 阶段加轻量锁,但批量插入(INSERT ... SELECT)仍用表级AUTO_INC锁,高并发时可能成瓶颈 - 若用
ALTER TABLE ... AUTO_INCREMENT = N调整起始值,仅对下一次插入生效,不影响已有数据页布局
真正有效的递增替代方案(非 UUID)
如果业务强依赖全局唯一、不可预测或分布式 ID,又想避开页分裂,优先考虑这些落地方式:
- 用
BIGINT+ 雪花算法(snowflake)服务生成 ID,并确保各节点起始时间戳和机器 ID 组合后,整体趋势递增(注意:时钟回拨会导致重复或乱序) - MySQL 8.0+ 可结合
UUID_TO_BIN(UUID(), TRUE),第二个参数TRUE会将 UUID 的时间戳段前置,使二进制值具备一定局部有序性 - 分库分表场景下,用
shard_id * 10000000000 + AUTO_INCREMENT构造复合递增 ID,前提是shard_id固定且单调增长 - 绝对避免在应用层拼接字符串后塞进
CHAR主键 —— 即便看起来“越来越长”,B+ 树比较的是字典序,不是时间序
页分裂不是慢查询日志里能直接看到的问题,它藏在 Innodb_page_splits 状态变量和 SHOW ENGINE INNODB STATUS 的 BUFFER POOL SECTION 里。真正要验证效果,得看 Pages made young 和 Pages not made young 的比值 —— 递增主键下,后者应显著占优。











