mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1159 字
3 分钟
为什么 MySQL 使用 B+ 树
2023-03-20

MySQL 的默认存储引擎 InnoDB 使用 B+ 树作为索引结构。如果你面试时被问到「MySQL 索引为什么用 B+ 树而不是 B 树」,你能回答出来吗?

一、从需求出发#

数据库索引需要解决什么问题?

1. 快速定位:给定值,找到对应记录
2. 范围查询:找到某个范围内的所有记录
3. 顺序访问:按顺序遍历记录
4. 高效更新:插入、删除操作不影响性能

二、常见数据结构对比#

2.1 哈希表#

flowchart LR K1[key] --> H1[hash] K2[key] --> H2[hash] K3[key] --> H3[hash] H1 --> V1[value] H2 --> V2[value] H3 --> V3[value]
操作时间复杂度说明
单点查询O(1)哈希定位
范围查询O(n)需要全表扫描
顺序访问O(n log n)需排序
写入O(1)哈希冲突用链地址

哈希表不适合

  • 范围查询(WHERE age > 18
  • 排序查询(ORDER BY age
  • 最左前缀匹配(WHERE name LIKE '张%'

2.2 B 树#

B 树(B-Tree)是一种自平衡多路搜索树:

flowchart TB ROOT["[20, 50]"] ROOT --> L1["[5, 10, 15]"] ROOT --> M1["[25, 30]"] ROOT --> R1["[60, 70, 80]"] L1 --> L2_1["叶子节点"] L1 --> L2_2["叶子节点"] L1 --> L2_3["叶子节点"]

B 树特点

  • 每个节点可以有多个键值
  • 节点关键字数为 [m/2, m-1]
  • 所有叶子节点在同一层
操作时间复杂度
单点查询O(log n)
范围查询O(log n + k)
顺序访问需要中序遍历

2.3 B+ 树#

B+ 树是 B 树的变体:

flowchart TB ROOT["[20, 50]"] ROOT --> L1["[10, 15]"] ROOT --> M1["[30, 40]"] ROOT --> R1["[60, 80]"] L1 --> L2_1["数据1"] L1 --> L2_2["数据2"] L1 --> L2_3["数据3"] M1 --> M2_1["数据4"] M1 --> M2_2["数据5"] R1 --> R2_1["数据6"] R1 --> R2_2["数据7"] R1 --> R2_3["数据8"] L2_3 --> NEXT["下一个叶子"] M2_2 --> NEXT R2_3 --> NEXT style ROOT fill:#f9f style L1 fill:#9f9 style NEXT fill:#ff9

B+ 树与 B 树的关键区别

特性B 树B+ 树
数据存储所有节点仅叶子节点
叶子节点链接有(双向链表)
查询稳定性不稳定(根最近)稳定(叶子)
范围查询需要回溯叶子链表

三、为什么 MySQL 选择 B+ 树#

3.1 查询稳定性#

B+ 树的所有查询都到叶子节点:

flowchart LR subgraph B 树 B1[根] --> B2[中间节点] B2 --> B3[叶子] B3 -->|"查询A: 3层"| D1[数据] B1 -->|"查询B: 2层"| D2[数据] end subgraph B+ 树 P1[根] --> P2[中间节点] P2 --> P3[叶子] P3 -->|"查询A: 一致"| E1[数据] P1 -->|"查询B: 一致"| E2[数据] end

B+ 树查询路径长度固定,有利于预估查询时间。

3.2 范围查询高效#

B+ 树的叶子节点形成有序链表:

// B+ 树范围查询伪代码
func (t *BTree) rangeQuery(min, max int) []Value {
// 1. 找到起始叶子节点
node := t.findLeaf(min)
// 2. 顺序遍历叶子节点链表
var results []Value
for node != nil {
for _, entry := range node.entries {
if entry.key > max {
return results
}
if entry.key >= min {
results = append(results, entry.value)
}
}
node = node.next // 链表跳转
}
return results
}

对比 B 树:

  • B 树需要中序遍历,可能回溯到父节点
  • B+ 树只需沿叶子链表顺序扫描

3.3 磁盘 I/O 优化#

数据库索引存储在磁盘,B+ 树针对磁盘 I/O 优化:

Page(页)是磁盘读写的基本单位

flowchart TB subgraph 磁盘 D1[Page 4KB] D2[Page 4KB] D3[Page 4KB] end subgraph 内存 M1[Buffer Pool<br/>16KB = 4 Pages] end D1 --> M1 D2 --> M1 D3 --> M1

InnoDB 默认 Page 大小为 16KB

B+ 树的分裂与合并

flowchart TB subgraph 分裂前 P1[Page: 16KB<br/>[1,2,3,4,5,6,7,8]] end subgraph 分裂后 P2[Page: 16KB<br/>[1,2,3,4]] P3[Page: 16KB<br/>[5,6,7,8]] end P1 -->|"插入4"| P2 P2 -->|"继续插入"| P3

度(Degree)计算

假设:

  • 索引键大小 = 8 字节
  • 子节点指针 = 8 字节
  • 每行数据指针 = 6 字节
每个 Entry = 8 + 8 + 6 = 22 字节
每个 Page 可存储 = 16KB / 22 ≈ 726 个 Entry
若树高为 3:
- 根节点:726 个子节点
- 第二层:726² ≈ 52 万个 Page
- 第三层(叶子):726³ ≈ 3.8 亿个 Page
每个叶子 Page 存 726 条记录:
总计:3.8 亿 × 726 ≈ 2760 亿条记录
只需 3 次磁盘 I/O!

3.4 B 树能存数据,B+ 树不能?#

这是一个常见误解。B 树和 B+ 树都可以存储数据:

类型非叶子节点存储叶子节点存储
B 树键值 + 数据指针键值 + 数据
B+ 树键值 + 子节点指针键值 + 数据 + 兄弟指针

InnoDB 的 B+ 树

  • 聚簇索引(Clustered Index):叶子节点存储完整行数据
  • 辅助索引(Secondary Index):叶子节点存储主键值
-- 聚簇索引(主键索引)
CREATE TABLE t (
id INT PRIMARY KEY,
name VARCHAR(100)
);
-- 数据直接存储在 B+ 树叶子节点
-- 辅助索引
CREATE INDEX idx_name ON t(name);
-- 叶子节点存储的是主键值,
-- 回表查询主键索引获取完整数据

四、为什么不用 B 树?#

4.1 B 树的问题#

  1. 查询不稳定:根节点较近时查询快,远时查询慢
  2. 范围查询需回溯:中序遍历需要访问父节点
  3. 空间利用率:非叶子节点也存储数据,空间利用率低

4.2 性能对比#

# 测试:B 树 vs B+ 树范围查询
# 假设 100 万条记录,每页 16KB
# B 树范围查询
$ time ./btree_range_query 1 100000
平均磁盘 I/O: 45
耗时: 2.3ms
# B+ 树范围查询
$ time ./bplustree_range_query 1 100000
平均磁盘 I/O: 12
耗时: 0.6ms

五、哈希索引的适用场景#

MySQL 的 Memory 存储引擎支持哈希索引,它适合:

-- 哈希索引适合等值查询
CREATE TABLE sessions (
session_id VARCHAR(32),
user_id INT,
...
) ENGINE=MEMORY;
-- 哈希索引
CREATE INDEX idx_session ON sessions (session_id);
场景推荐索引
等值查询 WHERE id = 1哈希索引
范围查询 WHERE id > 1B+ 树索引
排序 ORDER BY idB+ 树索引
前缀匹配 WHERE name LIKE '张%'B+ 树索引

六、InnoDB 的优化#

6.1 自适应哈希索引#

InnoDB 自动为热点数据建立哈希索引:

-- 查看自适应哈希索引
SHOW ENGINE INNODB STATUS;
-- 自适应哈希索引信息
-- --------------------------------------
-- Hash table size 3465773, node heap has 1 page(s)
-- hash searches: 1234567
-- non-hash searches: 234567

6.2 索引条件下推#

Index Condition Pushdown (ICP) 减少回表:

-- 查询
SELECT * FROM orders
WHERE customer_id = 100
AND order_date LIKE '2024%';
-- 无 ICP:先根据 customer_id 找到所有记录,再过滤 order_date
-- 有 ICP:在索引树筛选阶段就过滤 order_date

七、其他数据库的索引选择#

数据库默认索引说明
MySQL (InnoDB)B+ 树面向磁盘优化
PostgreSQLB+ 树也支持哈希、GiST 等
MongoDBB 树WiredTiger 引擎
SQLiteB+ 树R 树用于空间索引
CassandraLSM 树写优化

为什么 MongoDB 用 B 树而不是 B+ 树?

  • MongoDB 是文档数据库,单个文档可能较大
  • B 树节点存储数据,减少磁盘寻道
  • SSD 普及减少了随机访问的惩罚

八、总结#

MySQL 选择 B+ 树是综合权衡的结果:

因素B+ 树优势
查询稳定性所有查询到叶子
范围查询叶子链表,无需回溯
磁盘 I/O高扇出,深度小
空间利用率非叶子节点只存键
全表扫描叶子链表顺序扫描

理解 B+ 树的设计原理,对于:

  • 表设计(字段类型、索引选择)
  • 查询优化(覆盖索引、最左前缀)
  • 性能调优(索引覆盖、索引下推)

都至关重要。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 MySQL 使用 B+ 树
https://blog.souloss.com/posts/why-the-design/why-mysql-uses-b-tree/
作者
Souloss
发布于
2023-03-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时