页分裂是B+树索引正常扩容机制,非异常;插入导致页分裂引发碎片和性能下降,常被误认为索引失效,实际应关注分裂频率、填充因子合理性及主键设计是否加剧分裂。

插入操作本身不会导致索引“失效”,但会引发页分裂,进而造成索引碎片、查询性能下降——这常被误认为“索引不工作了”。真正该关心的是:页分裂是否高频发生?填充因子是否在帮倒忙?
页分裂不是异常,而是B+树索引的正常行为
当新数据插入到已满的数据页中,SQL Server 或 MySQL(InnoDB)必须拆分该页,把约一半行移到新页。这个过程叫页分裂。它本身不报错、不中断写入,但会带来两个实际后果:
- 外部碎片:新页在磁盘上通常不连续,后续范围扫描需跳转读取多个物理分散页
- 内部碎片:分裂后两页都未填满,空间利用率下降,缓存效率变低
- 分裂过程要加锁、写日志、更新父节点,高并发插入时会成为瓶颈
注意:sys.dm_db_index_physical_stats 中 avg_page_space_used_in_percent 持续低于 75%,且 page_count 增长快于数据量增长,就是页分裂频繁的信号。
填充因子(FILLFACTOR)不是防分裂的保险丝
很多人设 FILLFACTOR = 80 想“预留空间防分裂”,结果适得其反:
- 对 OLTP 表,低填充因子浪费内存和磁盘:每页少存 20% 行,意味着更多页要加载进 Buffer Pool,缓存命中率下降
- 它只对极少量、离散的随机插入有点用;一旦空闲空间被填满,下一次插入照样分裂,而且此时碎片更难整理
- 聚集索引若用
VARCHAR(50)订单号做主键,即使 FILLFACTOR=50,也挡不住因字符串排序不稳定导致的频繁重排与分裂
建议:OLTP 系统默认保持 FILLFACTOR = 0(即 100%),靠主键设计规避分裂,而非靠预留空间硬扛。
哪些主键设计会让插入直接触发高频页分裂
问题不在“有没有索引”,而在“索引按什么排序”——聚集索引的顺序决定插入位置。以下设计极易引发分裂:
-
VARCHAR类主键(如订单号'ORD202605230001'):长度大、排序非单调,新值可能插在任意中间位置 -
GUID(uniqueidentifier)主键:随机生成,几乎每次插入都落在不同页,分裂不可避免 - 复合主键中把低区分度字段放最左(如
(status, id)):相同status的大量行挤在同一页,很快撑爆
可落地的改进:
- 优先用
BIGINT IDENTITY或SEQUENCE作为隐藏聚集键,业务主键设为非聚集索引 - 若必须用 GUID,启用
NEWSEQUENTIALID()(SQL Server)或UUID_TO_BIN(uuid, 1)(MySQL 8.0+)降低随机性 - 时间戳前缀 + 自增后缀构造业务键(如
'20260523-00001'),兼顾可读性与插入局部性
验证页分裂影响比调参更重要
不要一看到慢查询就改填充因子或重建索引。先确认是不是页分裂在捣鬼:
- 查 DMV:
SELECT * FROM sys.dm_db_index_operational_stats(DB_ID(), OBJECT_ID('orders'), NULL, NULL),重点关注leaf_allocation_count和nonleaf_allocation_count - 对比同一张表不同时间段的
avg_page_space_used_in_percent:如果从 95% 一路掉到 65%,说明分裂已持续发生 - 观察
Page Splits/sec性能计数器(SQL Server)或Innodb_pages_written(MySQL)是否突增
真正难处理的,是那些“看起来没分裂、但查询变慢”的情况——比如统计信息过期 + 数据倾斜 + 联合索引顺序不合理,这时改填充因子完全无效,反而掩盖了根本问题。

















