再次理解 MySQL B+ 树:以每页 10 行的聚簇索引为例

1. 目标与范围

  • 仅讨论 InnoDB 主键聚簇索引的 B+ 树。
  • 统一采用"每页 10 行"的缩小版模型,保证叶子节点完整呈现。
  • 用图与数字说明树的演化、节点存储内容、查找路径、树高增长条件,以及为何选择 B+ 树而非哈希/B 树,并补充其他关系型数据库的结构对比。

2. 模型设定与术语

  • 表结构(简化):

    sql 复制代码
    CREATE TABLE t_order (
      id BIGINT PRIMARY KEY
      -- 其他列省略
    );
  • 统一"玩具模型"参数:

    • 每个叶子页最多存 10 行记录。
    • 每个中间页(Internal)最多指向 4 个叶子页。
    • Root 最多指向 3 个中间页。
  • 术语解释:

    • Root:根页;树的入口。
    • Internal:中间页;只存"分隔键 + 子页指针",不存整行。
    • Leaf:叶子页;聚簇索引的叶子页存整行数据。
    • 分隔键:一般可理解为"右侧子页的最小键",用于决定下探方向。

3. 树的演化:从只有 Root 到出现中间页与完整叶子页

3.1 高度=1:只有 Root(Root=Leaf)

  • 插入 id=1..10,单页即可容纳。
  • Root 即叶子页,直接存整行。
graph TD R["Root(兼叶子: 记录ID 1~10)"]

节点内容:

  • Root(叶子):存整行记录 1~10。

3.2 高度=2:Root 变为非叶,出现叶子页分裂

  • 插入 id=1..40,需要 4 个叶子页:
    • Leaf1: 1~10
    • Leaf2: 11~20
    • Leaf3: 21~30
    • Leaf4: 31~40
  • Root 存分隔键 [11, 21, 31],指向 4 个叶子页。
graph TD R["Root(分隔键: 11, 21, 31)"] R --> L1["Leaf(1~10)"] R --> L2["Leaf(11~20)"] R --> L3["Leaf(21~30)"] R --> L4["Leaf(31~40)"]

节点内容:

  • Root(非叶):分隔键 [11, 21, 31];子指针 → Leaf1..Leaf4。
  • Leaf:各页存 10 条整行记录,并按 id 有序串联。

3.3 高度=3:加入中间页(Internal),完整叶子节点展示

  • 插入 id=1..120 → 12 个叶子页(每叶 10 条)。
  • 设每个中间页管理 4 个叶子页 → 需要 3 个中间页。
  • Root 管 3 个中间页 → 分隔键 [41, 81]。
graph TD R["Root(分隔键: 41, 81)"] R --> I1["Internal(覆盖: 1~40; 分隔键: 1, 11, 21, 31)"] R --> I2["Internal(覆盖: 41~80; 分隔键: 41, 51, 61, 71)"] R --> I3["Internal(覆盖: 81~120; 分隔键: 81, 91, 101, 111)"] I1 --> L1["Leaf(1~10)"] I1 --> L2["Leaf(11~20)"] I1 --> L3["Leaf(21~30)"] I1 --> L4["Leaf(31~40)"] I2 --> L5["Leaf(41~50)"] I2 --> L6["Leaf(51~60)"] I2 --> L7["Leaf(61~70)"] I2 --> L8["Leaf(71~80)"] I3 --> L9["Leaf(81~90)"] I3 --> L10["Leaf(91~100)"] I3 --> L11["Leaf(101~110)"] I3 --> L12["Leaf(111~120)"]

节点内容:

  • Root:分隔键 [41, 81];子指针 → I1, I2, I3。
  • Internal:
    • I1:分隔键 [1, 11, 21, 31];子指针 → L1..L4。
    • I2:分隔键 [41, 51, 61, 71];子指针 → L5..L8。
    • I3:分隔键 [81, 91, 101, 111];子指针 → L9..L12。
  • Leaf:每页 10 条整行记录,按主键有序,叶叶之间链表相连。

4. 完整查找过程示例:查找 id=45

  • Root 分隔键 [41, 81] → 45 ∈ [41, 81) → 下探 I2。
  • I2 分隔键 [41, 51, 61, 71] → 45 ∈ [41, 51) → 下探 L5。
  • L5 覆盖 41~50 → 页内二分 → 命中 id=45 → 返回整行。
graph TD S["开始: 查找ID=45"] S --> R["访问Root(分隔键: 41, 81)"] R --> IR["比较: 45 ∈ [41,81) → 选择 Internal2"] IR --> I2["访问Internal2(分隔键: 41, 51, 61, 71)"] I2 --> IL["比较: 45 ∈ [41,51) → 选择 Leaf5"] IL --> L5["访问Leaf5(范围: 41~50)"] L5 --> F["页内二分 → 命中ID=45 → 返回整行"]

5. 何时树高会大于 3 层?

  • 玩具模型:
    • Root 最多挂 3 个中间页;每中间页最多挂 4 个叶子页;每叶 10 行。
    • 高度=3 的最大记录数:3 × 4 × 10 = 120 行。
    • 插入第 121 行需要第 13 个叶子页 → 至少 4 个中间页 → Root 无法再挂 → 必须增加一层,变为高度=4。
  • 真实 InnoDB(16KB 页,BIGINT 主键):
    • 叶子页容量:约 80~300 行(视行大小)。
    • 内节点扇出:约 500~1500(键+子指针条目数)。
    • 高度=3 的容量数量级:rowsPerLeaf × internalFanout × rootFanout ≈ 几千万到两亿。
    • 当数据量接近/超过该上限(如 10^9 级别),Root×Internal 挂不住所有叶子页 → 自动增加一层中间节点,树高升至 4。

6. 为什么选择 B+ 树(而不是哈希 / B 树)?

6.1 与哈希的对比:哈希不适合作主索引

  • 只擅长等值查询,无法支持范围、有序访问(BETWEEN、</>、ORDER BY、MIN/MAX、前缀匹配等)。
  • 无序随机访问,极不利于磁盘/SSD 的顺序读与预读,I/O 成本高。
  • 联合索引的左前缀裁剪在哈希中失效,优化空间小。
  • 扩容/重哈希代价大,不适合长周期 OLTP。
  • InnoDB 的做法:底层统一 B+ 树,缓冲池层派生"自适应哈希索引"加速热点等值查询。

6.2 与 B 树的对比:B+ 树更适合页组织与范围扫描

  • 内节点仅存"键+指针",不存整行 → 扇出高、树高低 → 页 I/O 少。
  • 查找路径一致性(总到叶子止步) → 锁与并发控制更简单。
  • 叶子有序链表 → 范围扫描/排序扫描可顺序预读,减少随机 I/O。
  • B 树中内节点可能存数据 → 扇出降低、路径不稳定 → 不利于磁盘组织。

6.3 与预读、减少 I/O 的关系(选择 B+ 树的关键根因)

  • 叶子顺序存放 + 链表:范围扫描能触发线性/随机预读,批量读入后续页。
  • 高扇出降低树高:访问页数少,Root/部分中间页常驻缓冲池,实际 I/O 多在叶层。
  • 覆盖/顺序访问:在二级索引中,叶子即可返回结果,进一步降低数据页 I/O(本文聚焦主键聚簇索引,不展开)。

结论:B+ 树在"页组织 + 混合负载 + 磁盘/SSD"场景下,兼顾范围能力、低树高、预读友好与并发事务兼容性,是通用关系型数据库的最优平衡选择。


7. 其他关系型数据库的数据结构与替代方案

7.1 主流 RDBMS 的索引结构

  • PostgreSQL:默认 B-Tree(实现风格与 B+ 树高度相似,数据集中在叶),页组织与范围扫描友好。
  • SQL Server:聚簇/非聚簇索引用 B-Tree(工程实现同样接近 B+ 树),页/区(extent)管理与预读。
  • Oracle:常用 B-Tree 索引,页组织模式相近。

结论:主流关系型数据库在通用 OLTP/OLAP 混合负载下,均选择 B+ 树或非常接近的变体。

7.2 面向特定场景的替代结构

  • LSM 树(RocksDB/MyRocks):写密集型优(顺序写+合并),读放大与压缩带来复杂权衡。
  • Fractal Tree / Bε-Tree(TokuDB):提升写吞吐与延迟稳定性,但实现复杂且读特性不同。
  • Bw-Tree(SQL Server Hekaton):无锁变体,适合内存并发场景。
  • ART(Adaptive Radix Tree):高性能内存索引,适用于KV/稀疏键空间。
  • 跳表/Trie/Zone Map/Segment Tree:多为列存/过滤/特定查询加速的补充结构。

结论:确有"更适合特定负载"的结构,但在"通用关系型数据库 + 页组织 + 混合负载"下,B+ 树仍然最均衡、最成熟。


8. 总结要点

  • B+ 树的层次分工:
    • Root/Internal:只存"分隔键 + 子页指针",不存整行;
    • Leaf:聚簇索引的叶子即整行,按主键有序并链表相连。
  • 演化过程:从 Root=Leaf(高度=1)→ Root+Leaf(高度=2)→ Root+Internal+Leaf(高度=3)。
  • 查找路径:Root → Internal → Leaf;页内二分命中记录。
  • 树高增长:受页容量与扇出限制;数据量大到超过 Root×Internal 能挂的叶子数时,自动升为更高层。
  • 选择 B+ 树的根因:范围能力、有序访问、扇出/低树高、预读友好与事务并发生态兼容;哈希/B 树各有长处但不满足通用 RDBMS 的综合需求。
  • 其他结构:在特定场景(写多读少、内存KV等)可优于 B+ 树,但不替代其在通用关系型数据库中的主地位。
相关推荐
q***31832 小时前
MySQL---存储过程详解
数据库·mysql
q***42822 小时前
MySQL数据库误删恢复_mysql 数据 误删
数据库·mysql·adb
Hello.Reader3 小时前
在 Flink Standalone 集群上运行 Flink CDC从下载到跑起一个 MySQL→Doris 同步任务
大数据·mysql·flink
2***d8853 小时前
使用 MySQL 从 JSON 字符串提取数据
mysql·oracle·json
小满、3 小时前
MySQL :锁机制、InnoDB 架构与 MVCC 解析
数据库·mysql·innodb·mvcc·锁机制
Sam_Deep_Thinking4 小时前
在 MySQL 里,不建议使用长事务的根因
mysql
5***r9354 小时前
开源数据同步中间件(Dbsyncer)简单玩一下 mysql to mysql 的增量,全量配置
mysql·中间件·开源
2301_789380499 小时前
vsc中使用DBHub-MCP连接云Mysql到vsc-copilot
数据库·mysql