返回首页

11 | B+Tree 与联合索引

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 更适合数据库的原因:

  1. 非叶子节点不存整行数据,因此每个节点能容纳更多 key,树更矮。
  2. 树更矮意味着从根到叶子的页访问次数更少。
  3. 叶子节点有序且链式连接,非常适合范围查询。
  4. 所有数据都在叶子节点,查询路径更稳定。

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';

执行过程通常是:

  1. 先在二级索引 idx_name 中找到 name='Tom'
  2. 从二级索引叶子节点拿到主键 id
  3. 再根据 id 回到聚簇索引中找到整行
  4. 取出 age

第 3 步再次访问聚簇索引的过程,就叫回表


4. 联合索引(复合索引)

4.1 什么是联合索引

联合索引就是把多个列按照一定顺序组合起来,建立在同一棵 B+Tree上。

例如:

CREATE INDEX idx_name_age_class ON student(name, age, class_id);

这不是建立了 3 棵树,而是建立了 1 棵树,只不过它的排序规则是:

  1. 先按 name 排序
  2. name 相同再按 age 排序
  3. 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 不在这个索引里,因此通常需要:

  1. 先在二级索引中找到主键值
  2. 再回到聚簇索引中取出 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. 高频总结

  1. B+Tree 是 InnoDB 最核心的索引结构,本质是多路平衡查找树。
  2. 非叶子节点只存 key 和指针,叶子节点存数据,并按顺序链式连接。
  3. 相比 B-Tree,B+Tree 更适合数据库的范围查询和磁盘页管理。
  4. 聚簇索引叶子节点存整行数据,二级索引叶子节点存索引列值和主键值。
  5. 联合索引底层仍是一棵 B+Tree,只是排序规则变成多个列的组合排序。
  6. 联合索引是否高效,关键在于是否满足最左前缀原则。
  7. 覆盖索引不需要回表,通常性能更好。
  8. 函数运算、隐式转换、前导模糊匹配、跳过最左列等,都是索引失效高频场景。

11. 小结

这一节真正要掌握的不是“背定义”,而是理解下面这条逻辑链:

  • B+Tree 为什么适合数据库
  • InnoDB 怎样用 B+Tree 组织聚簇索引和二级索引
  • 联合索引为什么和列顺序强相关
  • 最左前缀、覆盖索引、回表为什么会直接影响 SQL 性能

如果这些点都能连起来,那么你在看 EXPLAIN、设计索引、分析慢 SQL 时,思路就会更清楚。


📌 学习笔记声明 本文为个人学习整理的笔记,内容参考官方文档、教材及网络资料,仅供学习交流使用。 如有错误或不准确之处,欢迎在评论区指正,感谢支持!

上一篇

10| 索引原理与类型

下一篇

12| 执行计划 EXPLAIN 分析