一、核心概念
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-3次IO)
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+树执行:
- 定位起始节点:3次IO
- 顺序扫描:约20次IO(连续)
- 总时间:0.05秒
假设红黑树:
- 中序遍历跨层:10000次IO(随机)
- 总时间: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+树
- 磁盘IO优化
- 多路平衡树,降低树高度
- 100万数据仅需3-4层
- 查询稳定在3-4次磁盘IO
- 范围查询高效
- 叶子节点双向链表
- 顺序IO,性能优异
- 天然支持ORDER BY
- 非叶子节点容量大
- 只存索引和指针
- 单页存1000+索引
- 减少树高度
- 查询性能稳定
- 所有查询必到叶子节点
- 路径长度一致
- 延迟可预测
对比其他结构
| 结构 | 查询 | 范围查询 | 磁盘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+树成为磁盘数据库索引的最佳选择。