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        |
+------------------------+

想要获取整行怎么办?

必须“回表”:

  1. 在二级索引树找到 PK
  2. 到主键聚簇索引树里再查一遍

这就是回表的本质。


⭐ 第五章:一条查询在 B+Tree 中的真实路径

例如:

SELECT * FROM user WHERE age = 30;

步骤:

  1. 根据 age 进入二级索引 B+Tree
  2. 找到所有符合 age=30 的记录(叶子节点)
  3. 每条记录拿到主键值,例如 PK=2
  4. 回到聚簇索引 B+Tree
  5. 按 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)

posted @ 2025-12-06 13:46  中登程序猿  阅读(0)  评论(0)    收藏  举报