1159 字
3 分钟
为什么 MySQL 使用 B+ 树
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 树的问题
- 查询不稳定:根节点较近时查询快,远时查询慢
- 范围查询需回溯:中序遍历需要访问父节点
- 空间利用率:非叶子节点也存储数据,空间利用率低
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 > 1 | B+ 树索引 |
排序 ORDER BY id | B+ 树索引 |
前缀匹配 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: 2345676.2 索引条件下推
Index Condition Pushdown (ICP) 减少回表:
-- 查询SELECT * FROM ordersWHERE customer_id = 100 AND order_date LIKE '2024%';
-- 无 ICP:先根据 customer_id 找到所有记录,再过滤 order_date-- 有 ICP:在索引树筛选阶段就过滤 order_date七、其他数据库的索引选择
| 数据库 | 默认索引 | 说明 |
|---|---|---|
| MySQL (InnoDB) | B+ 树 | 面向磁盘优化 |
| PostgreSQL | B+ 树 | 也支持哈希、GiST 等 |
| MongoDB | B 树 | WiredTiger 引擎 |
| SQLite | B+ 树 | R 树用于空间索引 |
| Cassandra | LSM 树 | 写优化 |
为什么 MongoDB 用 B 树而不是 B+ 树?
- MongoDB 是文档数据库,单个文档可能较大
- B 树节点存储数据,减少磁盘寻道
- SSD 普及减少了随机访问的惩罚
八、总结
MySQL 选择 B+ 树是综合权衡的结果:
| 因素 | B+ 树优势 |
|---|---|
| 查询稳定性 | 所有查询到叶子 |
| 范围查询 | 叶子链表,无需回溯 |
| 磁盘 I/O | 高扇出,深度小 |
| 空间利用率 | 非叶子节点只存键 |
| 全表扫描 | 叶子链表顺序扫描 |
理解 B+ 树的设计原理,对于:
- 表设计(字段类型、索引选择)
- 查询优化(覆盖索引、最左前缀)
- 性能调优(索引覆盖、索引下推)
都至关重要。
参考资料
- High-Performance MySQL — MySQL 性能优化经典
- B+ 树可视化 — 交互式理解
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
为什么 MySQL 使用 B+ 树
https://blog.souloss.com/posts/why-the-design/why-mysql-uses-b-tree/ 部分信息可能已经过时
相关文章 智能推荐
1
为什么 MongoDB 使用 B 树
技术科普 深入解析 MongoDB 选择 B 树而非 B+ 树的设计原因,理解文档数据库的访问模式。
2
为什么 PostgreSQL 使用 MVCC
技术科普 深入解析多版本并发控制的设计原理,理解 PostgreSQL 如何实现高并发事务处理。
3
为什么数据库不应该使用外键
技术科普 深入解析为什么现代互联网应用中不建议使用外键,以及如何替代外键实现数据一致性。
4
为什么 MySQL 自增主键不单调也不连续
技术科普 深入解析 MySQL InnoDB 自增主键为什么不单调也不连续,以及事务和锁的影响。
5
为什么数据库会丢失数据
技术科普 深入解析数据库丢失数据的场景与原因,WAL、fsync、缓冲池等机制与数据安全。






