自增主键不能完全避免页分裂,但能大幅降低频次——因其保证新记录总追加到最右叶子页末尾,仅当该页满时才触发“尾部满裂”;而uuid等随机主键需搜索定位,90%以上插入落在中间已满页,强制分裂并引发i/o放大、锁竞争与二级索引膨胀。

自增主键真能避免页分裂吗
不能完全避免,但能大幅降低频次——关键在「插入位置可控」。InnoDB的B+树聚簇索引按主键值排序并物理存储数据,自增ID保证新记录总追加到最右叶子页末尾,只要该页还有空间,就不会触发分裂;而UUID或随机字符串插入时需先搜索定位,大概率落在中间已满页里,强制分裂。
常见错误现象:SHOW ENGINE INNODB STATUS 中反复出现 Pages split;innodb_page_splits 持续上涨;Data_free 占表总大小比例长期 >5% 且缓慢爬升。
- 事务回滚导致的ID跳空(如
INSERT后ROLLBACK)不会引发页分裂,只留下逻辑间隙 - 单行高并发插入下,即使自增ID也会因最右页填满(默认
innodb_fill_factor = 100)而分裂,这是“尾部满裂”,非设计缺陷 - 批量插入(如
INSERT ... SELECT或 LOAD DATA)比单行插入更友好,因InnoDB可预分配连续页
为什么innodb_fill_factor=100反而容易分裂
默认值100表示页写满才停,没给后续追加留余地。高并发写入时,多个事务几乎同时发现最右页已满,争抢触发分裂,造成锁等待和I/O毛刺。
实操建议:生产环境可设为 85~90,让每页预留10%~15%空间容纳紧随其后的自增记录,显著压低分裂概率。
- 修改方式:
SET GLOBAL innodb_fill_factor = 85;(需 SUPER 权限,重启失效)或写入配置文件my.cnf的[mysqld]段后重启 - 该参数仅影响叶节点填充率,不影响非叶节点;对已有数据无效,只对后续INSERT生效
- 设太低(如
50)会人为增加页数、抬高树高,范围查询性能反而下降
自增主键必须配合正确的建表与写入方式
光有 AUTO_INCREMENT 不够,字段类型、初始值、应用写法都影响效果。
- 主键列必须是
BIGINT UNSIGNED(非INT),避免21亿上限提前耗尽,导致ID翻转或溢出报错 - 禁止显式插入主键值:
INSERT INTO t(id, name) VALUES(100, 'x')—— 这会破坏递增连续性,下次自增可能插进中间页 - 不建议用
REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE频繁更新主键所在行,虽不改ID,但会引发二级索引维护开销 - 迁移旧表时,若原主键非自增,应新增
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY FIRST,再用ALTER TABLE ... DROP PRIMARY KEY, ADD PRIMARY KEY(id)重建聚簇索引
连接字段或业务ID不能套用自增逻辑
把 user_id 或 order_no 改成 AUTO_INCREMENT 是典型误用。自增只对**主键**生效,且仅优化其自身B+树的插入路径;外键列、JOIN字段、唯一业务码属于二级索引,其写入仍按值分布,无法享受尾部追加。
例如:订单表 orders 的 user_id 是普通索引,即便 users.id 是自增,orders 表按 user_id 插入仍是随机落页,照样分裂。
- 真正有效的做法:确保被JOIN的维度表(如
users)主键自增,且关联字段(orders.user_id)建为普通索引而非唯一索引 - 若必须按时间范围JOIN(如
created_at),优先建前缀索引(如INDEX idx_created_date (created_at)),并控制单次批量写入量 ≤ 1000 行 - 监控重点不是主键索引,而是高频JOIN字段的
Data_free—— 它涨得快,说明二级索引页已严重稀疏
最容易被忽略的点:页分裂不是“一次性的结构问题”,而是持续发生的I/O事件。哪怕你调了 innodb_fill_factor,如果应用还在用 INSERT ... VALUES() 单行循环插入百万数据,缓冲池争用和redo写放大依然存在。批量、有序、克制显式ID写入,三者缺一不可。











