🔥 一句话总纲
B+Tree 就像图书馆书架:书(行)放满了一格(页) → 再放就得搬书、再开新格 → 结构调整就是页分裂/合并。
1️⃣ B+Tree 页结构回顾
每个页(Page):
- 默认大小 16KB
- 叶子节点存索引列 + 行指针(或全部列,聚簇索引)
- 页满了,无法再插入 → 触发 页分裂
2️⃣ 页分裂(Page Split)
💡 核心理念
“一个页装不下新数据,就拆成两个页,把一半数据搬到新页。”
举例:
页原本:[1,2,3,4,5,6,7,8]
插入 9 → 页满
分裂成:左页:[1,2,3,4]右页:[5,6,7,8,9]
- 新页要插入到父节点 → 父节点可能也满 → 父节点递归分裂
- 最坏情况可能上升到根节点 → 树高 +1
🔥 影响
- 写入慢:拆页需要修改父节点、更新 B+Tree 链表
- 随机写多 → 脏页多 → Buffer Pool 压力大
- 页膨胀 = B+Tree 越来越大 → 查询高度增加(I/O 多)
3️⃣ 页分裂触发场景
-
顺序插入
- 自增主键 → 只在最右叶子插入 → 右侧页分裂 → 高性能稳定
-
随机插入
- 主键非自增 → 中间页频繁分裂 → 随机 I/O 多 → 写性能下降
-
二级索引
- 插入任意位置 → 页分裂更频繁
4️⃣ 页合并(Page Merge / Page Compression)
分裂后,有些页可能使用率很低:
- 删除数据 → 页只剩 1~2 条
- 页利用率 < 50% → 可合并左/右页 → 减少 B+Tree 膨胀
特点:
- 异步进行(后台线程)
- 避免频繁分裂导致树过高、随机 I/O 过多
- 过度删除 + 插入 → 仍可能反复分裂/合并 → 写入性能波动
5️⃣ 脑补动画版(写入膨胀全过程)
- 初始页
[1,2,3,4,5,6,7,8]
- 插入 9 → 分裂
左页: [1,2,3,4] 右页: [5,6,7,8,9]
- 父节点更新
父页: [4,8] (指向左页、右页)
- 随机插入 3.5 → 中间页再次分裂
中间页:[3,4] → [3] [4] 两页
父页需更新 → 树高度可能增加
- 删除大量 5~7 → 页合并
左页: [1,2,3,4] 右页: [8,9]
🎯 结果:树高可能减少,但页空洞仍存在 → 查询仍慢于理想状态
6️⃣ 为什么索引会慢慢膨胀?
- B+Tree 页分裂是不可避免的,尤其是 随机插入或二级索引
- 随着数据增长,索引页越来越多 → 树高增加 → 查询深度增加 → I/O 多 → 查询慢
- 删除数据也不一定立即收缩 → 空洞页存在 → 内存/磁盘占用大
- 写入频繁 → 分裂/合并频繁 → 写性能下降
7️⃣ 工程师优化策略
| 问题 | 解决方式 |
|---|---|
| 顺序插入不均衡 | 尽量用自增主键/预分配 key |
| 二级索引膨胀 | 控制索引列大小,减少随机插入 |
| 页利用率低 | 定期 OPTIMIZE TABLE 或 rebuild 索引 |
| 写入性能下降 | 批量插入,减少单条随机写 |
🔥 总结一句话
页分裂和页合并是 B+Tree 自我调整机制,保证查询仍然对 I/O 友好,但随机写、二级索引、大量删除会让树膨胀 → 写慢、查询慢、空间浪费。