一、为什么要理解索引结构?
我们平常写下的 SQL:
sql
SELECT * FROM user WHERE id = 100;
虽然只是一行查询语句,但 MySQL 背后是如何"知道"该去哪一页找数据的?
为什么即使有上千万行,也能瞬间查到?
核心答案只有三个字:B+ 树。
二、索引的本质是什么?
在 InnoDB 引擎中:
- 索引(Index)不是"附加文件",而是数据的有序组织结构;
- MySQL 通过 B+ 树(Balanced Plus Tree) 实现高效查找;
- 所有表数据都存储在主键 B+ 树的叶子节点中。
一张表,其实就是一棵以主键为索引的 B+ 树。
三、建表示例
sql
CREATE TABLE A (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
age INT
) ENGINE=InnoDB;
id
是主键;- MySQL 自动为它建立聚簇索引(Clustered Index)。
四、插入数据:B+ 树的起点
sql
INSERT INTO A (name, age) VALUES ('Tom', 18);
INSERT INTO A (name, age) VALUES ('Jerry', 22);
表中数据:
id | name | age |
---|---|---|
1 | Tom | 18 |
2 | Jerry | 22 |
初始结构(单页)
ini
┌────────────────────────────────┐
│ Root/Leaf Page(同一个页) │
│--------------------------------│
│ id=1 | name=Tom | age=18 │
│ id=2 | name=Jerry | age=22 │
└────────────────────────────────┘
只有一个页(Page),所有数据都在这个叶子页中。
五、页分裂:B+ 树如何"长大"
InnoDB 的每个数据页大小为 16KB ,
一行约 60 字节,一个页能存 约 270 行。
当插入第 271 行时,页满了,触发 页分裂(Page Split)。
分裂前
bash
Page#1
┌─────────────────────────────┐
│ id=1 ~ id=270 │
│ (已满) │
└─────────────────────────────┘
分裂后
ini
Root Page
┌───────────────┐
│ key = 135 │
└───────────────┘
/ \
/ \
┌────────────────┐ ┌────────────────┐
│ Page#1: id 1~135 │ │ Page#2: id 136~270 │
└────────────────┘ └────────────────┘
此时树高 = 2 层(Root + Leaf)。
六、数据继续增长:形成三层结构
当页越来越多时,Root 页也可能装不下所有指针,
此时会再次分裂,形成 3 层结构。
less
Root
(Level 3)
│
┌───────────────┴───────────────┐
▼ ▼
Internal Page#10 Internal Page#20
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│Leaf Page#1 │──▶ ... ──▶│Leaf Page#N │
└────────────┘ └────────────┘
- Root:保存中间节点指针
- Internal Node:保存页号范围
- Leaf Node:保存完整行数据
- 所有 Leaf 之间用双向链表连接,便于范围查询。
七、普通索引(二级索引)结构
再创建一个索引:
sql
CREATE INDEX idx_name ON A(name);
生成一棵独立的 B+ 树(存储 name
和对应主键 id):
bash
idx_name(二级索引)
Root
│
┌──────────┴──────────┐
▼ ▼
name=Jerry,id=2 name=Tom,id=1
二级索引的叶子节点不存整行数据,只存"索引列 + 主键 id"。
当用 name 查找时,需要回表。
回表示意
sql
SELECT * FROM A WHERE name = 'Tom';
执行过程:
- 在 idx_name 树查
name='Tom'
→ 得到id=1
- 去主键树查
id=1
→ 获取完整行
ini
idx_name树: name='Tom' → id=1
↓
主键树: id=1 → (Tom, 18)
八、B+ 树查找过程
以三层结构为例:
css
[Root]
│
┌───┴───┐
▼ ▼
[Internal] [Internal]
│
▼
[Leaf Pages]
查找 id=900000
时:
- 从 Root 判断区间;
- 进入对应的 Internal Page;
- 定位到 Leaf Page;
- 顺序扫描到匹配记录。
全程仅需 3 次磁盘 I/O(树高 3)。
九、页分裂与性能影响
插入模式 | 分裂频率 | 碎片 | 性能 |
---|---|---|---|
自增主键 | 极少 | 少 | ✅ 最优 |
随机 UUID | 频繁 | 多 | ❌ 慢 |
批量插入 | 可控 | 少 | 👍 稳定 |
✅ 建议使用自增主键,保证插入顺序、减少分裂与碎片。
十、百万级数据的树高度
层级 | 每节点页数 | 可容纳行数 |
---|---|---|
叶子页 | 270 行 | 270 |
第二层 | 1000 页 | 27 万 |
第三层 | 1000 页 | 2.7 亿 |
百万级数据时,树高通常只有 3 层。
查找只需 3 次磁盘 I/O。
十一、整体结构回顾图
css
[Root]
│
┌────────────────┴────────────────┐
▼ ▼
[Internal#1] [Internal#2]
│ │
▼ ▼
[Leaf#1] ⇄ [Leaf#2] ⇄ ... ⇄ [Leaf#N]
- Root:索引入口
- Internal:保存页指针
- Leaf:存放整行数据
- 叶子之间用双向链表连接(便于范围扫描)
十二、总结
概念 | 说明 |
---|---|
聚簇索引 | 表数据即索引叶子 |
二级索引 | 需通过主键回表 |
页分裂 | 数据页满后自动拆页 |
B+ 树高度 | 通常 3~4 层即可支持亿级数据 |
自增主键 | 能保持索引有序、减少分裂 |
十三、一句话总结
在 MySQL 8 中,索引不是"附加文件",
而是数据本身的有序存储结构。
理解 B+ 树结构,就理解了 MySQL 的性能之魂。