InnoDB 索引 B+Tree 全剖析
⭐ 第一章:为什么必须是 B+Tree 而不是 B-Tree / Hash?
1. Hash(均匀分布)
- 优点:O(1) 查找
- 缺点致命:不支持范围查询、排序、前缀匹配
WHERE age BETWEEN 10 AND 18→ 完全废掉
2. B-Tree(每个节点都保存数据)
- 层级深
- 每次查找会把节点全部读入内存
- 叶子节点不联结 → 顺序扫描慢
3. B+Tree(InnoDB 采用)
✔ 所有数据都在叶子节点(固定大小页)
✔ 非叶节点只保存索引 key,让树更矮
✔ 叶子节点通过双向链表相连 → 范围查找超级高效
✔ 批量 IO、预读能力更强
一句话:B+Tree 更“磁盘友好”,这是数据库选它的根本理由。
⭐ 第二章:InnoDB 索引底层存储单位 —— Page(页)
理解任何索引问题,都必须从 Page 开始。
InnoDB 一页固定 16KB。
一个节点 = 一个页
!(示意图)无法画图,用文字结构表达如下:
┌──────────────────────────┐
│ Page Header(记录数量等) │
├──────────────────────────┤
│ Directory Slots(记录目录)│
├──────────────────────────┤
│ 行记录1 │
│ 行记录2 │
│ ... │
└──────────────────────────┘
一个 B+Tree 节点就是一个 Page。
所以:
- Page 越大 → 每个节点能容纳更多 key → 树越矮
- 树越矮 → 查找越快(通常高度 2~4)
⭐ 第三章:聚簇索引(主键索引)结构
InnoDB 规定:
表数据本身就是一棵按主键排序的 B+Tree(聚簇索引)。
也就是:
主键索引的叶子节点上存放“整行记录”。
结构图:
[Root]
/ \
[Node] [Node]
/ \ / \
[Leaf: PK, row] [Leaf: PK, row] ...
叶子节点结构:
+-----------------------------+
| PK: 1 | name: Tom | age 20 |
+-----------------------------+
| PK: 2 | name: Bob | age 30 |
+-----------------------------+
...
数据按 PK 顺序紧密存放。
⭐ 第四章:二级索引(普通索引)结构
二级索引的叶子节点不存整行,而是:
叶子节点存储:索引列 + 主键值
结构如下:
Leaf page:
+------------------------+
| age: 20 | PK: 1 |
+------------------------+
| age: 20 | PK: 9 |
+------------------------+
| age: 30 | PK: 2 |
+------------------------+
想要获取整行怎么办?
必须“回表”:
- 在二级索引树找到 PK
- 到主键聚簇索引树里再查一遍
这就是回表的本质。
⭐ 第五章:一条查询在 B+Tree 中的真实路径
例如:
SELECT * FROM user WHERE age = 30;
步骤:
- 根据 age 进入二级索引 B+Tree
- 找到所有符合
age=30的记录(叶子节点) - 每条记录拿到主键值,例如 PK=2
- 回到聚簇索引 B+Tree
- 按 PK 找整行记录
流程图(文字版):
二级索引(B+Tree) ——找到PK→ 聚簇索引(B+Tree)
⭐ 第六章:为什么“回表”贵?
因为:
- 二级索引查一次 = 若干次磁盘页 IO
- 回表再查一次主键 = 再来若干次 IO
假设二级索引页高 = 3
主键页高 = 3
一次语句需要:
3+3 = 6次 page 访问(注意是最小情况)
如果 age=30 有 1000 行?
→ 就得回表 1000 次(糟糕)
所以覆盖索引非常重要。
⭐ 第七章:覆盖索引的威力
例如:
SELECT age FROM user WHERE age = 30;
如果只查二级索引里的字段(age 已经在二级索引里),不用回表。
称为 覆盖索引。
效果:
- 不回表 → 大幅减少 IO → 查询速度翻倍甚至几十倍
- 执行计划显示:
Using index
实际企业中会专门设计复合索引用来“覆盖重要查询”。
⭐ 第八章:复合索引是怎么存的?
例如:
CREATE INDEX idx_name_age ON user(name, age);
二级索引的 key 顺序是:
name → age → 主键
所以它的排序实际是:
(name1, age1, PK1)
(name1, age2, PK9)
(name2, age3, PK5)
...
这直接解释了:
- 最左前缀原则
- 为什么 name = ? AND age = ? 能走索引
- 为什么 age = ? 不行
这是 B+Tree 排序规则决定的。
⭐ 第九章:页分裂与页合并
当你插入数据时:
- 如果 16KB 页满了 → 分裂为两个页
- 分裂后 B+Tree 层级可能会上升(树变高)
分裂代价:
- IO 变多
- 页不连续 → 碎片化 → 查询变慢
- CPU 内存消耗增加
所以:
👉 使用自增主键顺序插入可以最大化避免页分裂
👉 UUID 主键特别容易造成页分裂,性能差
这就是为什么阿里规范极度推荐:
强制使用递增主键(bigint 自增)。
⭐ 第十章:为什么不要用 UUID 做主键?
简单一句:
UUID 是随机写,会疯狂触发页分裂。
效果:
- Page 几乎每次都被打断成碎片
- B+Tree 越长 → IO 越多
- 写入性能降几十倍
真实场景你如果用了 UUID 当主键,一定会踩到“插入性能极差”。
⭐ 第十一章:一张图总结全部核心逻辑
(文字图示)
二级索引树 (索引列 + PK)
|
| 回表
v
聚簇索引树 (整行记录)
^
|
Page
16KB
⭐ 最后:你现在掌握了哪些?
✔ 为什么是 B+Tree
✔ 聚簇索引 vs 二级索引
✔ 回表本质
✔ 覆盖索引
✔ 最左前缀原则
✔ Page 分裂
✔ 为什么自增主键最佳
✔ 为什么 UUID 糟糕
✔ 执行计划 cost 如何计算(基于 B+Tree 层级 + 回表 IO)
浙公网安备 33010602011771号