说一个不少人可能存在的认知偏差:自增主键并不是万能的防分裂屏障,页分裂依然会发生。但坦白讲,它确实能将分裂的频率与代价降到最低。关键不在于更换主键类型,而在于如何合理运用填充率、写入节奏以及锁模式这几个核心工具。
划重点:自增主键本身无法彻底杜绝页分裂,但能显著减少分裂次数并降低其开销;关键在于控制页填充率、批量写入节奏以及锁模式的协同配合。

为什么AUTO_INCREMENT主键依旧会引发页分裂
B+树的叶节点大小是固定的,默认16KB。即使你插入的是严格递增的ID,当最右边的叶节点写满时,InnoDB也必须执行分裂操作——这并非设计缺陷,而是B+树维持平衡的必然机制。许多人误以为“自增就一定不会分裂”,但在高并发插入场景下,你只要观察一下innodb_page_splits这个指标,数值照样会持续攀升。
这里有几个容易被忽略的细节:
- 页通常填充到约93.75%时(预留了1/16的空间)就会触发分裂,并非等到100%满了才分裂
- 单条
INSERT INTO t VALUES ()每次执行都要检查页剩余空间,这种频繁的检查本身就会带来额外开销 - 事务未提交时,purge线程无法回收该页的空闲空间,这间接抑制了后续页合并的机会
调低innodb_fill_factor让页“预留空间”
该参数控制新建索引页的初始填充比例,默认值为100,即填满为止。如果你将其设为85,意味着叶节点只存放85%的数据,剩下的15%空间预留给后续的自增插入。直接效果是:分裂概率降低。
需要注意几个实操要点:
- 该参数是动态生效的:
SET GLOBAL innodb_fill_factor = 85,但它仅对后续CREATE TABLE或ALTER TABLE ... FORCE重建的索引起作用 - 对于已存在的表,你需要执行
ALTER TABLE t ENGINE=InnoDB或ALTER TABLE t FORCE才能应用新的填充率 - 注意不要调得过低,低于75时磁盘占用和缓冲池压力会明显增加。经验表明:在没有监控数据支撑的情况下,不要盲目调低
用批量INSERT代替单行AUTO_INCREMENT
逐条插入时,每一条记录都要经历“页定位→空间检查→可能分裂”的流程。批量插入则不同,它能将多条记录一次性纳入同一轮页检查,相当于把分裂的开销摊薄了。
实操建议:
- 推荐单次
INSERT INTO t VALUES (),(),()...写入100到500行(具体受max_allowed_packet参数限制) - 避免在长事务中混杂大更新操作,否则会阻塞purge线程,导致已删除记录的那些页空间无法及时回收用于合并
- 如果业务上确实无法使用批量写入,只能逐条插入,则请确认
innodb_autoinc_lock_mode=2(交错模式)是否已开启。这能减少自增锁的争用,避免锁等待把分裂的延迟放大
监控innodb_page_splits与innodb_page_merge_attempts是否真实改善
优化效果好不好,别用QPS或平均延迟来猜测,应该直接关注InnoDB的底层指标:
- 直接查看
SHOW GLOBAL STATUS LIKE 'Innodb_page_splits',观察数值是否下降 - 同时查看
SHOW GLOBAL STATUS LIKE 'Innodb_page_merge_attempts',如果该值上升,说明空闲页正在被有效合并 - 针对关键表,执行
SELECT INDEX_NAME, N_RECS, PAGE_NO FROM information_schema.INNODB_INDEX_STATS WHERE TABLE_NAME='t',估算单页记录数是否接近理论值(主键为int类型时,大约每页700到900条)
页分裂无法彻底消除。但如果你能将innodb_page_splits从每秒几十次的水平压到个位数,同时innodb_page_merge_attempts同步上升,那才是真正的优化信号。容易被忽视的一点是:如果已有表不重建索引,你调低innodb_fill_factor再低也不会生效,因为它根本不会作用于旧索引。
