1. 为什么这一节很重要
在 408 计算机考研知识体系里,索引相关内容可以和数据结构、数据库系统、外存访问、查找效率分析一起理解。MySQL 中最核心、最常见的索引结构就是 B+Tree。
在 InnoDB 中:
- 主键索引通常对应聚簇索引
- 普通索引、唯一索引、联合索引通常属于二级索引
- 大多数高性能的等值查询、范围查询、排序查询,都和 B+Tree 有直接关系
所以,这一节既是面试高频点,也是数据库优化的基础。
2. B+Tree 数据结构原理
2.1 什么是 B+Tree
B+Tree 是一种多路平衡查找树,可以看作 B-Tree 的改进版本,特别适合数据库这种基于磁盘页进行存储和访问的系统。
它有几个关键特征:
- 非叶子节点只保存键值和子节点指针,不保存真实数据
- 所有真实数据都保存在叶子节点中
- 所有叶子节点按照键值顺序连接成链表
- 整棵树始终保持平衡,查询路径长度稳定
2.2 节点结构
非叶子节点
非叶子节点的作用主要是“导航”。
可以抽象理解为:
[10 | 20 | 30]
/ | | <10 10~20 20~30 >30
这里的 10、20、30 只是分隔值,用来决定下一步该往哪个子节点继续查找。
叶子节点
叶子节点中才真正存放索引对应的数据。
可以抽象理解为:
[1, 3, 5, 8] -> [10, 12, 15] -> [20, 22, 30]
叶子节点之间顺序连接,这一点对范围查询特别重要。
2.3 叶子节点链表为什么重要
如果执行范围查询,比如:
SELECT * FROM student WHERE id BETWEEN 10 AND 100;
数据库先通过树结构快速定位到第一个满足条件的叶子节点,然后顺着叶子节点链表继续往后扫描即可。
好处是:
- 不用每读一条记录都重新从根节点查找
- 范围查询效率高
- 顺序扫描、排序访问更自然
2.4 B+Tree 与 B-Tree 的对比
| 对比项 | B-Tree | B+Tree |
|---|---|---|
| 数据存储位置 | 所有节点都可能存数据 | 只有叶子节点存数据 |
| 非叶子节点作用 | 导航 + 可能存数据 | 只负责导航 |
| 范围查询能力 | 一般 | 更强 |
| 叶子节点链表 | 不是重点特征 | 非常关键 |
| 单节点可容纳 key 数量 | 相对更少 | 相对更多 |
| 数据库适配性 | 较好 | 更适合 |
2.5 为什么数据库更偏爱 B+Tree
从 408 的视角,本质要抓住一个关键词:减少磁盘 IO。
B+Tree 更适合数据库的原因:
- 非叶子节点不存整行数据,因此每个节点能容纳更多 key,树更矮。
- 树更矮意味着从根到叶子的页访问次数更少。
- 叶子节点有序且链式连接,非常适合范围查询。
- 所有数据都在叶子节点,查询路径更稳定。
3. InnoDB 中 B+Tree 的具体实现
3.1 页(Page)思想
InnoDB 的数据和索引并不是按“行”直接组织在磁盘上的,而是按页组织。默认页大小通常是 16KB。
可以理解为:
- 一个 B+Tree 节点通常对应一个页
- 查询索引时,本质是在不同页之间跳转
- 优化索引,本质上就是尽量减少页访问次数
所以,数据库索引优化和操作系统中的外存管理思维是相通的。
3.2 聚簇索引
在 InnoDB 中,主键索引通常就是聚簇索引。
例如:
CREATE TABLE student (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
class_id INT
);
它的特点是:
- 表数据本身按照主键顺序组织在一棵 B+Tree 中
- 叶子节点中存放的是整行记录
- 一张表只能有一个聚簇索引
也就是说,主键索引的叶子节点里,实际保存的是:
(id, name, age, class_id)
聚簇索引的优点
- 按主键查询速度快
- 主键范围查询效率高
- 找到叶子节点就相当于拿到了整行数据
3.3 二级索引
除主键索引以外,普通索引、唯一索引、联合索引通常都属于二级索引。
例如:
CREATE INDEX idx_name ON student(name);
这时二级索引叶子节点中存储的通常不是整行数据,而是:
(name, id)
也就是:
- 索引列值
- 对应记录的主键值
3.4 聚簇索引 vs 二级索引
| 对比项 | 聚簇索引 | 二级索引 |
|---|---|---|
| 常见代表 | 主键索引 | 普通索引、唯一索引、联合索引 |
| 叶子节点内容 | 整行数据 | 索引列值 + 主键值 |
| 是否可能回表 | 一般不需要 | 经常可能需要 |
| 数量限制 | 一张表只能一个 | 可以有多个 |
3.5 什么是回表查询
例如有索引:
CREATE INDEX idx_name ON student(name);
执行 SQL:
SELECT age FROM student WHERE name = 'Tom';
执行过程通常是:
- 先在二级索引 idx_name 中找到 name='Tom'
- 从二级索引叶子节点拿到主键 id
- 再根据 id 回到聚簇索引中找到整行
- 取出 age
第 3 步再次访问聚簇索引的过程,就叫回表。
4. 联合索引(复合索引)
4.1 什么是联合索引
联合索引就是把多个列按照一定顺序组合起来,建立在同一棵 B+Tree上。
例如:
CREATE INDEX idx_name_age_class ON student(name, age, class_id);
这不是建立了 3 棵树,而是建立了 1 棵树,只不过它的排序规则是:
- 先按 name 排序
- name 相同再按 age 排序
- name 和 age 都相同再按 class_id 排序
4.2 联合索引的底层存储结构
对于 InnoDB 的二级联合索引,叶子节点中可以抽象理解为保存:
(name, age, class_id, 主键id)
注意:
- 联合索引中的列顺序非常重要
- 底层有序性是从左到右建立的
- 最后通常还会带上主键值,用于唯一定位记录并支持回表
例如有数据:
| id | name | age | class_id |
|---|---|---|---|
| 1 | Alice | 18 | 2 |
| 2 | Alice | 19 | 1 |
| 3 | Bob | 18 | 2 |
| 4 | Bob | 20 | 3 |
那么索引 (name, age, class_id) 在逻辑上近似按下面顺序排列:
(Alice, 18, 2, 1)
(Alice, 19, 1, 2)
(Bob, 18, 2, 3)
(Bob, 20, 3, 4)
4.3 创建语法
可以在建表时创建:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
INDEX idx_user_status_time (user_id, status, create_time)
);
也可以单独创建:
CREATE INDEX idx_user_status_time
ON orders(user_id, status, create_time);
5. 最左前缀原则
5.1 基本概念
设有联合索引:
(user_id, status, create_time)
所谓最左前缀原则,就是:
查询条件必须从联合索引最左边的列开始连续匹配,才能较充分地利用索引。
可以把它想象成一本先按 user_id 排,再按 status 排,最后按 create_time 排的大字典。
5.2 哪些情况能走索引
情况 1:只使用最左列
SELECT * FROM orders WHERE user_id = 1001;
- 能走索引
- 因为命中了最左列 user_id
情况 2:使用前两列
SELECT * FROM orders WHERE user_id = 1001 AND status = 1;
- 能走索引
- 命中了 (user_id, status)
情况 3:使用前三列
SELECT * FROM orders
WHERE user_id = 1001 AND status = 1 AND create_time = '2026-01-01 10:00:00';
- 能走索引
- 命中了完整联合索引
情况 4:前两列等值,最后一列范围
SELECT * FROM orders
WHERE user_id = 1001 AND status = 1 AND create_time > '2026-01-01';
- 能走索引
- 这是 B+Tree 很典型的使用场景
5.3 哪些情况不能充分利用索引
情况 1:跳过最左列
SELECT * FROM orders WHERE status = 1;
- 通常不能高效利用 (user_id, status, create_time)
- 因为没有从 user_id 开始
情况 2:中间断开
SELECT * FROM orders WHERE user_id = 1001 AND create_time = '2026-01-01';
- user_id 可以利用
- 但中间缺少 status
- create_time 往往不能继续充分利用联合索引的有序性
情况 3:只查后缀列
SELECT * FROM orders WHERE create_time > '2026-01-01';
- 通常不能高效利用该联合索引
- 因为最左列没有参与
5.4 最左前缀的记忆方式
对于索引 (a, b, c),常见可高效利用的前缀是:
- (a)
- (a, b)
- (a, b, c)
通常不能直接高效利用的是:
- (b)
- (c)
- (b, c)
- (a, c) 的完整有序能力
6. 索引失效的常见场景
6.1 对索引列做函数运算
SELECT * FROM orders WHERE YEAR(create_time) = 2026;
这样写常常不利于索引使用,因为索引里保存的是原始 create_time,不是 YEAR(create_time) 的结果。
更合理的写法:
SELECT * FROM orders
WHERE create_time >= '2026-01-01'
AND create_time < '2027-01-01';
6.2 隐式类型转换
如果列是字符型,查询时却传入数字型值,可能引发隐式转换,从而影响索引使用。
例如 phone 是 VARCHAR 时,应尽量保证查询参数也是字符串类型。
6.3 前导模糊查询
SELECT * FROM student WHERE name LIKE '%Tom';
因为 B+Tree 是按前缀有序的,以百分号开头时,很难确定扫描起点。
而下面这种更容易利用索引:
SELECT * FROM student WHERE name LIKE 'Tom%';
6.4 不满足最左前缀
SELECT * FROM orders WHERE status = 1 AND create_time > '2026-01-01';
如果索引是 (user_id, status, create_time),由于跳过了 user_id,往往无法高效利用索引。
6.5 范围条件后继续匹配右侧列受限
如果索引是:
(a, b, c)
查询是:
WHERE a = 10 AND b > 20 AND c = 30
通常:
- a 可以高效使用
- b 可以用于范围定位
- c 往往无法继续充分利用有序性
6.6 OR 条件使用不当
SELECT * FROM orders WHERE user_id = 1001 OR amount > 500;
如果 amount 没有合适索引,优化器可能直接选择全表扫描。
6.7 数据量太小或选择性太差
即使语法上能走索引,优化器也未必会选。
例如:
- 表只有几十行
- 某列值重复度非常高
这时 MySQL 可能认为全表扫描成本更低。
7. 覆盖索引与回表查询
7.1 覆盖索引的概念
如果查询需要的列,都可以直接从索引中获取,那么就不需要回表,这种情况称为覆盖索引。
例如有索引:
INDEX idx_user_status_time (user_id, status, create_time)
执行:
SELECT user_id, status, create_time
FROM orders
WHERE user_id = 1001 AND status = 1;
因为返回列和筛选列都在索引里,所以只查二级索引即可,不需要再访问聚簇索引。
7.2 覆盖索引的好处
- 减少回表
- 减少随机 IO
- 提高查询性能
- 对高并发场景更友好
7.3 什么时候会回表
还是前面的索引:
INDEX idx_user_status_time (user_id, status, create_time)
如果执行:
SELECT amount
FROM orders
WHERE user_id = 1001 AND status = 1;
由于 amount 不在这个索引里,因此通常需要:
- 先在二级索引中找到主键值
- 再回到聚簇索引中取出 amount
这就是回表。
8. SQL 示例与 EXPLAIN 分析
8.1 建表与建索引
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
remark VARCHAR(200),
INDEX idx_user_status_time (user_id, status, create_time)
);
8.2 示例一:命中联合索引前缀
EXPLAIN
SELECT *
FROM orders
WHERE user_id = 1001 AND status = 1;
分析:
- 通常会使用 idx_user_status_time
- 可能看到 type 为 ref
- 因为是 SELECT *,所以大概率还要回表
常见需要关注的字段:
| 字段 | 含义 |
|---|---|
| key | 实际使用的索引 |
| key_len | 使用到的索引长度 |
| rows | 预估扫描行数 |
| Extra | 额外信息 |
8.3 示例二:覆盖索引
EXPLAIN
SELECT user_id, status, create_time
FROM orders
WHERE user_id = 1001 AND status = 1;
分析:
- 查询列全部在联合索引中
- 通常属于覆盖索引
- Extra 中经常可能看到 Using index
8.4 示例三:不满足最左前缀
EXPLAIN
SELECT *
FROM orders
WHERE status = 1;
分析:
- 跳过了最左列 user_id
- 可能无法有效利用联合索引
- 有时会出现 type=ALL,表示全表扫描
8.5 示例四:范围查询
EXPLAIN
SELECT user_id, status, create_time
FROM orders
WHERE user_id = 1001
AND status = 1
AND create_time >= '2026-01-01'
AND create_time < '2026-02-01';
分析:
- 前两列等值匹配,最后一列范围匹配
- 是 B+Tree 很典型的优势场景
- 如果返回列仍然都在索引中,往往仍可形成覆盖索引
8.6 示例五:函数导致索引利用不佳
EXPLAIN
SELECT *
FROM orders
WHERE DATE(create_time) = '2026-01-01';
优化写法:
EXPLAIN
SELECT *
FROM orders
WHERE create_time >= '2026-01-01 00:00:00'
AND create_time < '2026-01-02 00:00:00';
9. 用 408 视角串联理解
9.1 和数据结构的联系
- B+Tree 本质上是多路平衡查找树
- 核心目标是降低树高
- 树越矮,外存访问次数越少
9.2 和操作系统/组成原理的联系
- 数据库索引优化的核心之一就是减少磁盘 IO
- 一个节点通常对应一个页
- 顺序访问通常比随机访问更友好
- B+Tree 的链式叶子节点非常适合顺序扫描
9.3 和数据库系统的联系
- 聚簇索引决定数据物理组织方式
- 二级索引通常需要借助主键回表
- 联合索引要遵循最左前缀原则
- 覆盖索引是常见的重要优化手段
一句话总结:
B+Tree 之所以适合数据库,不只是因为“查找快”,更因为它和页式存储、范围扫描、磁盘 IO 优化高度契合。
10. 高频总结
- B+Tree 是 InnoDB 最核心的索引结构,本质是多路平衡查找树。
- 非叶子节点只存 key 和指针,叶子节点存数据,并按顺序链式连接。
- 相比 B-Tree,B+Tree 更适合数据库的范围查询和磁盘页管理。
- 聚簇索引叶子节点存整行数据,二级索引叶子节点存索引列值和主键值。
- 联合索引底层仍是一棵 B+Tree,只是排序规则变成多个列的组合排序。
- 联合索引是否高效,关键在于是否满足最左前缀原则。
- 覆盖索引不需要回表,通常性能更好。
- 函数运算、隐式转换、前导模糊匹配、跳过最左列等,都是索引失效高频场景。
11. 小结
这一节真正要掌握的不是“背定义”,而是理解下面这条逻辑链:
- B+Tree 为什么适合数据库
- InnoDB 怎样用 B+Tree 组织聚簇索引和二级索引
- 联合索引为什么和列顺序强相关
- 最左前缀、覆盖索引、回表为什么会直接影响 SQL 性能
如果这些点都能连起来,那么你在看 EXPLAIN、设计索引、分析慢 SQL 时,思路就会更清楚。
📌 学习笔记声明 本文为个人学习整理的笔记,内容参考官方文档、教材及网络资料,仅供学习交流使用。 如有错误或不准确之处,欢迎在评论区指正,感谢支持!