一、核心概念

B+树是一种多路平衡查找树,InnoDB使用它作为索引结构的核心数据结构。B+树的特点是:

  • 所有数据都存储在叶子节点
  • 叶子节点之间通过双向链表连接
  • 非叶子节点只存储键值和指针,不存储数据
  • 树的高度较低,通常为3-4层

二、为什么不用其他数据结构?

1. 为什么不用二叉搜索树(BST)?

问题:树的高度不可控

示例:插入有序数据
1 → 2 → 3 → 4 → 5

变成链表:
1
 \
  2
   \
    3
     \
      4
       \
        5

深度:O(n)
查询效率:退化为 O(n)
磁盘IO次数:最多5次(不可接受)

致命缺陷

  • 最坏情况退化为链表
  • 树高度与数据量成正比
  • 磁盘IO次数过多

2. 为什么不用平衡二叉树(AVL/红黑树)?

AVL树示例(100万条数据):
深度 = log₂(1000000) ≈ 20层

问题:
- 每层需要一次磁盘IO
- 查询一条数据:20次磁盘IO
- 每次IO耗时:约10ms
- 总耗时:200ms(不可接受)

对比:
B+树深度:3-4层
磁盘IO:3-4次
总耗时:30-40ms(可接受)

核心问题

  • 二叉树:每个节点只有2个子节点,树太高
  • 磁盘IO:是性能瓶颈(比内存访问慢10万倍)
  • 数据库场景:数据量巨大,需要降低树高度

3. 为什么不用B树?

B树 vs B+树对比

B树结构:
       [10, 30, 50]
      /    |    |   \
   [5]  [20]  [40]  [60]
   数据  数据  数据  数据

特点:
- 每个节点都存数据
- 叶子节点不连接

B+树结构:
       [10, 30, 50]
      /    |    |   \
   [5,10] [20,30] [40,50] [60,70]
   ↔——————↔——————↔——————↔
   (叶子节点双向链表)

特点:
- 非叶子节点只存索引
- 数据全在叶子节点
- 叶子节点链表连接

B+树的优势

特性 B树 B+树
单个节点能存放的索引数 少(存数据占空间) 多(只存索引)
树的高度 较高 较低
范围查询 需要中序遍历,跨层访问 叶子节点链表顺序扫描
查询稳定性 不稳定(可能在任意层找到) 稳定(必须到叶子节点)
磁盘IO 较多 较少

实际对比示例

-- 假设:页大小16KB,主键BIGINT(8字节),指针6字节

B树】
非叶子节点:
- 存储:键值(8B) + 指针(6B) + 部分数据(假设50B)
- 单个元素:64B
- 单页容量:16KB / 64B = 256

树高3层:256 × 256 × 256 = 1677

B+树】
非叶子节点:
- 存储:键值(8B) + 指针(6B)
- 单个元素:14B
- 单页容量:16KB / 14B  1170

树高3层:1170 × 1170 × 1170 = 16亿

结论:相同高度,B+树存储能力远超B

范围查询对比

-- 查询:SELECT * FROM users WHERE id BETWEEN 100 AND 200;

B树】
1. 定位到id=100的节点
2. 中序遍历(左→根→右)
3. 跨越多层,频繁磁盘IO

路径:Node  Node  Parent  Node  ...
磁盘IO:大量随机IO

B+树】
1. 定位到id=100的叶子节点
2. 沿着链表顺序扫描到id=200
3. 全程在叶子层,顺序IO

路径:Leaf  Leaf  Leaf  ...
磁盘IO:少量顺序IO(快10-100倍)

4. 为什么不用哈希表?

哈希表优势:
- O(1) 等值查询
- 内存中极快

哈希表缺陷:
❌ 不支持范围查询
   WHERE age > 20  -- 无法使用
   WHERE name LIKE 'Zhang%'  -- 无法使用

❌ 不支持排序
   ORDER BY create_time  -- 无法使用

❌ 哈希冲突
   需要额外处理

❌ 数据分布
   哈希值无序,磁盘存储碎片化

使用场景:
- Memory引擎的HASH索引
- 仅用于等值查询(key-value)

三、B+树的优势总结

1. 磁盘IO优化(最重要)

【案例】100万条数据查询

二叉树:
- 深度:20层
- 磁盘IO:20次
- 耗时:200ms

B+树:
- 深度:3层
- 磁盘IO:3次
- 节点1(根):内存缓存(0次IO)
- 节点2:1次IO
- 节点3(叶子):1次IO
- 实际耗时:20ms(快10倍)

关键

  • 每个节点对应一个磁盘页(16KB)
  • B+树的多路特性,单节点可存1000+索引
  • 树高度 = 磁盘IO次数

2. 范围查询高效

-- 业务常见查询
SELECT * FROM orders 
WHERE create_time BETWEEN '2025-11-01' AND '2025-11-02'
ORDER BY create_time;

B+树执行:
1. 定位起始叶子节点(2-3IO
2. 顺序扫描链表(连续IO,很快)
3. 数据天然有序,无需排序

效率:O(log n + m)  -- m为结果数量

3. 查询性能稳定

B树:
- 可能在任意层找到数据
- 查询时间不稳定
- 最好:O(1)
- 最坏:O(log n)

B+树:
- 必须到叶子节点
- 所有查询路径长度相同
- 时间稳定:O(log n)

稳定性对数据库很重要:
- 可预测的查询延迟
- 避免性能抖动

4. 顺序IO友好

磁盘IO特性:
- 顺序读写:100-200 MB/s
- 随机读写:0.5-5 MB/s
- 顺序比随机快 20-100倍

B+树叶子节点链表:
- 相邻节点物理存储接近
- 范围查询 → 顺序IO
- 预读机制:一次IO读取多个页

示例:
SELECT * FROM orders 
WHERE user_id=12345 
ORDER BY create_time 
LIMIT 100;

优化器:
- 利用(user_id, create_time)索引
- 顺序扫描叶子节点
- 触发预读,性能更好

5. 利用局部性原理

数据库访问特点:
- 80%的访问集中在20%的数据
- 热点数据频繁访问

B+树优化:
- 根节点:常驻内存
- 第2层:大概率在内存
- 第3层叶子节点:部分在内存(热数据)

实际查询:
- 根节点:0次IO(内存)
- 第2层:0-1次IO(大概率缓存)
- 叶子节点:0-1次IO(热数据缓存)

平均IO次数:1-2次(而非理论的3次)

四、实战性能对比

对比测试(100万条数据)

CREATE TABLE test_index (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT,
    create_time DATETIME
);

-- 插入100万条数据
-- ...

场景1:单条查询

-- InnoDB(B+树索引)
SELECT * FROM test_index WHERE id=500000;

执行计划:
- type: const
- rows: 1
- 磁盘IO: 3
- 时间: 0.002

-- 假设用红黑树
- 深度: 20
- 磁盘IO: 20
- 时间: 0.02秒(慢10倍)

场景2:范围查询

SELECT * FROM test_index 
WHERE id BETWEEN 100000 AND 110000;

B+树执行:
- 定位起始节点:3IO
- 顺序扫描:约20IO(连续)
- 总时间:0.05

假设红黑树:
- 中序遍历跨层:10000IO(随机)
- 总时间:10秒(慢200倍)

场景3:ORDER BY

SELECT * FROM test_index 
WHERE age > 20 
ORDER BY id 
LIMIT 100;

B+树(主键索引):
- 叶子节点天然有序
- 无需额外排序
- Extra: Using where(无Using filesort

其他结构:
- 需要额外排序
- 临时表 + 快速排序
- 额外的内存/磁盘开销

五、InnoDB的B+树实现细节

1. 页结构

【InnoDB页结构】(默认16KB)

+------------------+
| File Header(38B) | ← 页类型、校验和、LSN等
+------------------+
| Page Header(56B) | ← 槽数量、记录数、层级等
+------------------+
| Infimum(13B)    | ← 最小记录(虚拟)
+------------------+
| User Records    | ← 实际数据行
| ...             |
+------------------+
| Supremum(13B)   | ← 最大记录(虚拟)
+------------------+
| Free Space      | ← 未使用空间
+------------------+
| Page Directory  | ← 槽位目录(加速查找)
+------------------+
| File Trailer(8B)| ← 校验页完整性
+------------------+

单页容量计算:
- 可用空间:≈15KB
- 叶子节点:存完整行(假设1KB/行) → 15条
- 非叶子节点:仅存索引(14B/条) → 1170条

3层B+树容量:
1170 × 1170 × 15 ≈ 2050万行

2. 自适应哈希索引

InnoDB优化:
- 监控索引使用频率
- 对热点数据自动创建哈希索引
- 内存中的哈希表,O(1)查询

触发条件:
- 某个索引页被频繁访问
- 访问模式稳定(等值查询)

查看状态:
SHOW ENGINE INNODB STATUS\G

Adaptive Hash Index:
- hash searches/s: 1234
- non-hash searches/s: 56

3. Change Buffer

优化写入性能:
- 非唯一二级索引的更新
- 先写入Change Buffer(内存)
- 后台异步合并到B+树

避免:
- 随机IO
- 页分裂抖动

适用场景:
- 写多读少
- 二级索引更新频繁

六、面试要点总结

为什么选B+树

  1. 磁盘IO优化
    • 多路平衡树,降低树高度
    • 100万数据仅需3-4层
    • 查询稳定在3-4次磁盘IO
  2. 范围查询高效
    • 叶子节点双向链表
    • 顺序IO,性能优异
    • 天然支持ORDER BY
  3. 非叶子节点容量大
    • 只存索引和指针
    • 单页存1000+索引
    • 减少树高度
  4. 查询性能稳定
    • 所有查询必到叶子节点
    • 路径长度一致
    • 延迟可预测

对比其他结构

结构 查询 范围查询 磁盘IO 缺陷
二叉树 O(n) 退化链表
红黑树 O(log n) 多(20次) 树太高
B树 O(log n) 一般 一般 节点存数据
B+树 O(log n) ✅优秀 少(3次) ✅最优
哈希 O(1) - 不支持范围

关键数字

- InnoDB页大小:16KB
- 非叶子节点容量:1000-1200个索引
- 3层B+树:2000万行
- 根节点:常驻内存
- 平均磁盘IO:1-2次(缓存命中)

一句话总结

InnoDB选择B+树是因为它完美平衡了查询性能、范围查询、磁盘IO和数据有序性,在海量数据场景下,能将查询的磁盘IO稳定在3-4次,而叶子节点链表使得范围查询能够高效利用顺序IO,这些特性使B+树成为磁盘数据库索引的最佳选择。