文章目录

在这里插入图片描述

一篇用虚构示例讲清 InnoDB 索引与回表的入门笔记。全文只使用 student 表示例,不涉及任何业务表或项目代码。


写在前面:你要搞懂的一条主线

在 InnoDB 里,可以把一张表想象成 两(或多)棵 B+ 树

  1. 聚簇索引:叶子节点里放着 完整的一行数据(按主键排序)。
  2. 二级索引:叶子节点里只有 索引列 + 主键,没有你要的其他列。

查询时,如果只用了二级索引,却还需要 name 这类不在索引里的列,就要拿着主键 再到聚簇索引里取一次完整行——这一步就叫 回表

下面按「存储结构 → 索引类型 → 查询路径 → 如何避免回表 → 多索引怎么选」的顺序展开。


第 1 章:问题从哪来——「有索引为什么还慢?」

假设有这样一张学生表:

CREATE TABLE student (
  id        INT PRIMARY KEY,   -- 主键,InnoDB 下即聚簇索引
  name      VARCHAR(50),
  age       INT,
  class_id  INT,
  INDEX idx_age (age)        -- 在 age 上的普通索引(二级索引)
);

你执行:

SELECT name, age FROM student WHERE age = 18;

直觉上:age 有索引,应该很快。但有时你会发现:索引确实用上了,磁盘/缓冲池的读取次数却并不低。

原因在于:idx_age 这棵索引树里,并没有 name 这一列。索引帮你快速找到了「哪些学生的 age 是 18」,但若要拿到 name,往往还要再查一次主表数据——也就是回表。

本章 takeaway:索引不是整张表的复印件;它只保存「排序用的键」和「找到那一行所需的地址(主键)」。


第 2 章:B+ 树——索引的「目录」长什么样

2.1 为什么用 B+ 树,而不是二叉树?

表数据可能有百万、千万行。索引要支持:

  • 等值查询age = 18
  • 范围查询age BETWEEN 18 AND 20
  • 排序ORDER BY age

B+ 树的特点很适合这些场景:

特点 含义 带来的好处
多叉、树矮 每个节点有很多孩子,树的高度低 从根到叶通常只需几次磁盘 IO
数据都在叶子 非叶子节点只存「指路」的键 内存里能缓存更多「目录」节点
叶子形成链表 同一层的叶子按顺序串起来 范围扫描时顺着链表走,不用反复回根

可以把它想成 一本厚字典的拼音目录:中间页告诉你「去哪个字母区间」,真正的词条(数据)都在最后的叶子页上,而且叶子页是连续编号的,翻起来很顺。

在这里插入图片描述

2.2 索引本质上是什么?

索引 = 按某一列(或多列)排序的一棵 B+ 树。

  • 你查 WHERE age = 18,引擎在这棵树上定位到 age = 18 的叶子。
  • 叶子上记的不是「整行学生档案」,而是能继续找到这一行的线索——具体是什么线索,取决于这是聚簇索引还是二级索引(下一章讲)。

2.3 一个小例子:idx_age 的叶子里有什么?

idx_age 的 B+ 树 叶子 上,可能看到类似:

(age=17, id=2)
(age=18, id=3)
(age=18, id=7)
(age=19, id=12)
...

注意:这里有 ageid没有 name
这就是为什么后面会出现「回表」——索引只给了你 门牌号(id),没给你住户姓名(name)


第 3 章:聚簇索引——表数据本身就在一棵 B+ 树里

3.1 聚簇索引名称的由来

在 MySQL 里,「聚簇索引」这个名字的核心含义是:把索引和数据行「聚」在一起存放

1)为什么叫「聚簇」?
  • 英文是 clustered indexcluster 有「聚集、成团、成簇」的意思。
  • 聚簇索引的叶子节点直接存放数据行,而不是只存指向别处的指针。
  • 主键索引的结构与表数据绑定在一起,各行按主键顺序「聚集」在 B+ 树的叶子层。
2)和普通(二级)索引的区别
类型 叶子节点里有什么
普通索引(二级索引) 主键值或行定位信息;完整数据行另存于聚簇索引
聚簇索引 完整数据行;查到叶子页就等于查到数据
3)直观类比
  • 普通索引像书的目录:只告诉你正文在第几页。
  • 聚簇索引目录和正文合订本:翻到这一页就直接看到内容。
4)在 InnoDB 中的具体表现
  • InnoDB 的主键索引就是聚簇索引
  • 若没有显式主键,InnoDB 会按顺序选择聚簇键:
    1. 第一个非空唯一索引
    2. 若仍没有,则生成隐藏的 row_id(6 字节)作为聚簇索引键。

一句话:之所以叫聚簇索引,是因为表的数据记录与主键索引聚集在同一棵 B+ 树里,叶子节点直接保存数据行


3.2 三个容易混在一起的词

术语 一句话解释
主键(Primary Key) 唯一标识一行;在 InnoDB 里通常也充当聚簇索引的排序键。
聚簇索引键(Clustered Index Key) 决定「这一行在聚簇 B+ 树里排在哪」的键;常见就是主键 id
聚簇索引(Clustered Index) 那棵「叶子 = 完整行数据」的 B+ 树;表的主数据按这棵树的顺序存储。

在 InnoDB 中,可以暂时把三者理解成:主键 ≈ 聚簇索引键,聚簇索引 = 按主键组织的那棵「主数据树」

入门阶段先记住:有主键 id 时,聚簇索引就是按 id 排序;无主键时的选择规则见 3.1 节

3.3 聚簇索引的叶子长什么样?

student 表按主键 id 的聚簇索引,叶子节点里存的是 整行

id=1 → (name='李四', age=17, class_id=2)
id=2 → (name='王五', age=17, class_id=1)
id=3 → (name='张三', age=18, class_id=1)
id=7 → (name='赵六', age=18, class_id=3)
...

关键对比(请记住):

  • 聚簇索引的叶子 ≈ 数据行本身(或行的主要部分)
  • 二级索引的叶子 ≠ 整行,只有索引列 + 主键

3.4 类比:按门牌号排序的公寓楼

  • 门牌号 = 主键 id(聚簇索引键)
  • 每个房间里的全部信息 = 一行完整数据(name, age, class_id …)
  • 公寓楼按门牌号物理排列 = 聚簇索引按主键顺序存数据

你拿着 id=3 去找人,直接进 3 号房就能拿到张三的全部信息 —— 一次命中,不用再去别处抄档案

在这里插入图片描述


第 4 章:二级索引——另一棵 B+ 树,叶子只带「门牌号」

4.1 二级索引是什么?

age 列上建的 idx_age,是 独立于聚簇索引的另一棵 B+ 树

  • 排序键是 age(非主键);
  • 叶子上通常是:age 的值 + 主键 id(聚簇索引键)。
idx_age 的叶子(示例):

(age=18, id=3)
(age=18, id=7)
(age=19, id=12)

注意:这里没有 name

二级索引的作用:按 age 快速找到对应学生的 id 列表

4.2 两棵树如何配合?

SELECT name FROM student WHERE age = 18 时,逻辑上是:

步骤 1:在 idx_age(二级索引)上找 age=18 → 得到 id 列表 [3, 7, ...]
步骤 2:对每个 id,到聚簇索引(主数据树)上取完整行 → 读出 name

步骤 2 就是 回表:索引树 → 主数据树,多走一趟。

在这里插入图片描述


第 5 章:回表——定义、步骤与判断方法

5.1 正式定义

回表(Table Lookup / 回表查询) 是指:

  • MySQL 通过 二级索引 找到了满足条件的行的 主键值(聚簇索引键),但查询需要的列 不在该二级索引里,于是还要拿着主键,到 聚簇索引(主表数据) 中再读取完整那一行。

可以记成两步:

  1. 先查索引:在二级索引的 B+ 树里定位,拿到 id(主键);
  2. 再查表:用这些 id 到聚簇索引里取完整行(如 name 等列)。

5.2 同一个表,三种查询对比

SQL 主要使用的结构 是否回表 原因
SELECT id FROM student WHERE age = 18 idx_age 需要的 idage 都在 idx_age 叶子里。
SELECT name FROM student WHERE age = 18 idx_age → 聚簇索引 name 不在 idx_age 里,必须用 id 再取整行。
SELECT * FROM student WHERE id = 3 聚簇索引 直接按主键在聚簇树上取整行,不经过二级索引。

5.3 回表的成本直觉

age = 18 匹配 1000 行,且查询需要回表:

  • 二级索引上可能扫描 1000 个叶子条目(得到 1000 个 id);
  • 聚簇索引上大致要进行 1000 次「按主键找行」(实际实现会做一定优化,但直觉上仍是「匹配越多,回表越多」)。

因此:高选择性(过滤后剩很少行)时回表尚可接受;低选择性(一次命中成千上万行)且要很多列时,回表成本会非常明显。

5.4 一句话记忆

二级索引给你的是 地址(主键);回表是按地址去 主楼(聚簇索引)取包裹(完整行)

在这里插入图片描述


第 6 章:覆盖索引——如何故意避免回表

6.1 什么是覆盖索引?

SELECT 用到的列 + WHERE 用到的列,已经全部包含在某个索引里时,优化器可以只扫描这棵索引,不必再访问聚簇索引取行——这叫 索引覆盖(Covering Index),也就 不需要回表

例如建立联合索引:

INDEX idx_age_name (age, name)

则:

SELECT name FROM student WHERE age = 18;

可能只需扫描 idx_age_nameage 用于过滤,name 直接在索引叶子里,不必再按 id 回聚簇索引

若业务经常是「多个条件一起过滤」,只在各列上建孤立单列索引往往不够理想——第 7 章会说明优化器通常只选其中一个;此时设计良好的 联合索引 往往更能一次缩小候选集(是否还能覆盖,仍取决于 SELECT 列)。

6.2 代价与权衡

  • 好处:减少回表,降低 IO,对大结果集尤其明显。
  • 代价:索引变宽,占更多磁盘;写入时要维护更多索引页。

这是典型的 用空间换时间,需按查询模式设计,不是索引越多越好。


第 7 章:多个单列二级索引同时出现在 WHERE 里——先走哪个?

在这里插入图片描述

前面几章默认「WHERE 只命中一个二级索引」。实际开发里更常见的是:多个条件、每个条件上各自有一个单列二级索引。这时很多人会问:

agenameis_delete 都有索引,MySQL 会不会三个都走?还是先走某个、再走其他?

核心结论先放在这里:

这三个是独立的单列二级索引,不是联合复合索引。
在常见路径下,优化器不会同时走多个索引,只会选其中一个它认为最优的索引;其余条件在回表拿到完整行之后再过滤。

(Index Merge 等特殊策略入门阶段可先不纠结;把「单索引 + 回表 + 过滤」这条主路径记牢即可。)

7.1 表结构与 SQL(延续 student 示例)

在第 1 章的表上,再补两个单列索引和软删标记,变成:

CREATE TABLE student (
  id         INT PRIMARY KEY,
  name       VARCHAR(50),
  age        INT,
  class_id   INT,
  is_delete  TINYINT DEFAULT 0,
  INDEX idx_age (age),           -- 单列二级索引
  INDEX idx_name (name),         -- 单列二级索引
  INDEX idx_is_delete (is_delete) -- 单列二级索引
);

注意:这里是 三棵独立的二级索引树,不是 INDEX idx_age_name_delete (age, name, is_delete) 这种联合索引。

查询:

SELECT * FROM student
WHERE age = 18
  AND name = '张三'
  AND is_delete = 0;

7.2 完整执行步骤拆解

步骤 1:优化器估算——各索引大概要扫多少行

MySQL 会结合 索引统计信息 / 直方图,分别估算「若只走这个索引,大概命中多少行」:

候选索引 条件 估算命中行数(记为 N)
idx_age age = 18 N1(例如几百行)
idx_name name = '张三' N2(例如几乎唯一,约 1 行)
idx_is_delete is_delete = 0 N3(例如全表 90%,很大)

估算越准,越容易选对「最省代价」的那条路。

步骤 2:选择最优索引(核心规则)

优化器挑选 估算行数更少、整体代价更低 的那个索引,作为本次查询真正用来扫描的索引。

沿用上面的直觉数字:

  • name = '张三' 几乎唯一 → N2 ≈ 1
  • age = 18 几百行 → N1 较大
  • is_delete = 0 覆盖绝大部分数据 → N3 极大

最终选 idx_name 执行查询。

不是「三个索引轮流都扫一遍」,也 不是「先扫 age 再扫 name」。
正常是:只选一个最优单列索引

步骤 3:只扫描被选中的那条二级索引

以选中 idx_name 为例:

  1. idx_name 这棵二级索引 B+ 树,找到所有 name = '张三' 的叶子条目;
  2. 叶子节点存的是:索引列 name + 主键 id(和第 4 章一致);
  3. 此时手里只有一批主键 ID,还没有 ageis_delete,更没有整行。

对应前面已出现过的叶子形态:

idx_name 的叶子(示例):

(name='张三', id=3)
(name='李四', id=1)
...
步骤 4:回表——拿主键去聚簇索引取完整行

对每个候选 id,到 聚簇索引(主键索引) 上读取完整行,得到:

id=3 → (name='张三', age=18, class_id=1, is_delete=0)
...

这一步就是第 5 章讲的 回表:二级索引只给了门牌号,完整档案要回主楼取。

因为是 SELECT *,所需列不可能只靠 idx_name 覆盖(见第 6 章),一定要回表

步骤 5:内存里过滤「没用上索引」的条件

回表拿到完整行后,在 Server 层用剩余条件过滤:

age = 18 AND is_delete = 0
  • 符合:保留,进入结果集;
  • 不符合:丢弃,不返回给客户端。

也就是说:ageis_delete 并没有各自再走一遍自己的索引树;它们在「已经回表拿到的行」上做普通条件判断。

步骤 6:汇总返回客户端

所有通过过滤的行组成结果集,返回。

7.3 用一张图串起来

整条路径可以看成五步:估算 → 只选一个最优索引 → 扫二级索引拿 id → 回表取整行 → 内存过滤剩余条件

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对应关系再对照一下:

例: 选中 idx_name

通过

不通过

SQL: WHERE age=18 AND name='张三' AND is_delete=0

优化器分别估算 idx_age / idx_name / idx_is_delete

选估算代价最低的一个索引

扫描 name 二级索引 → 得到主键 id 列表

回表: 按 id 读聚簇索引完整行

内存过滤: age=18 AND is_delete=0

加入结果集

丢弃

返回客户端

7.4 和「联合索引」的关键对比

对比点 三个单列二级索引(本章) 联合索引 INDEX (name, age, is_delete)
索引树数量 三棵独立的二级索引树 通常一棵联合索引树
WHERE 多条件 一般 只选一个 索引扫,其余条件回表后过滤 可能在 同一棵 索引树上用多个列缩小范围
SELECT * 几乎总要回表 仍要回表拿其它列,但定位阶段可能更准

所以:列上「都建了索引」,并不等于「这次查询会用上全部索引」。若要少回表,还要和 第 6 章的覆盖索引 一起考虑(联合索引列是否覆盖 SELECT)。

7.5 本章 takeaway

  1. 多个单列二级索引 ≠ 联合复合索引。
  2. 常见执行路径:优化器选一个最优索引 → 二级索引取主键 → 回表取整行 → 剩余条件过滤
  3. 选择性高的条件(如几乎唯一的 name)更容易被选中;像 is_delete = 0 这种低选择性条件,单独走索引往往不划算。

第 8 章:总流程图与自测

8.1 查询走哪条路?(总览)

主键 / 聚簇索引

多个候选二级索引

收到 SQL

优化器选择哪个索引?

在聚簇 B+ 树按 id 定位

叶子即完整行 → 直接返回

估算各索引代价, 通常只选一个最优

在选中的二级索引 B+ 树扫描

所需列是否都在该索引中?

覆盖索引: 只读索引树

回表: 用主键 id 再查聚簇索引

还有未用索引条件?

回表后内存过滤剩余条件

拼出完整行

返回结果集

8.2 自测题(建议先闭卷再对答案)

Q1. 主键、聚簇索引键、聚簇索引在 student 表里分别对应什么?

参考答案
  • 主键:id
  • 聚簇索引键:一般就是 id
  • 聚簇索引:按 id 组织、叶子存完整行数据的那棵 B+ 树

Q2. idx_age 的叶子上有哪些列?没有什么?

参考答案
  • 有:age,以及主键 id
  • 没有:nameclass_id 等其他列

Q3. 什么情况下会回表?什么情况下不会?

参考答案
  • :使用二级索引,且 SELECT 还需要索引里不存在的列
  • 不会:只查索引里已有的列(覆盖索引);或直接用主键在聚簇索引上查

Q4. B+ 树叶子链表对 WHERE age BETWEEN 18 AND 20 有什么帮助?

参考答案

找到起始叶子后,可以沿链表顺序扫描到结束边界,而不必反复从根节点查找。

Q5. WHERE age=18 AND name='张三' AND is_delete=0,三个字段各自有单列二级索引时,会不会三个索引都走?流程是什么?

参考答案
  • 一般不会三个都走;优化器用统计信息估算后,通常 只选一个最优单列索引(例如选择性最好的 idx_name
  • 流程:扫描该二级索引拿主键 id → 回表读完整行 → 在内存里过滤其余条件(如 age、is_delete)→ 返回
  • 这与「建一个 (name, age, is_delete) 联合索引」不是一回事

8.3 30 秒口述检验

如果你能不看笔记说出下面这段话,说明已经掌握主线:

InnoDB 表数据按主键存在聚簇索引的 B+ 树里;age 上的二级索引是另一棵树,叶子只有 ageid。查 name 时要先用 age 找到 id,再按 id 去聚簇索引取整行,第二次就是回表。
若 WHERE 里多个单列二级索引条件同时出现,优化器通常只选一个最优索引扫树,回表后再过滤其余条件——「列上都有索引」不等于「这次查询把所有索引都用上」。


附录:术语速查表

中文 英文 在本文明中的含义
B+ 树 B+ Tree 索引背后的多叉平衡树,数据在叶子且叶子相连
聚簇索引 Clustered Index 叶子存整行,表数据按主键顺序组织
二级索引 Secondary Index 非主键索引,叶子多为「索引列 + 主键」
主键 / 聚簇索引键 PK / Clustered Key 定位一行在聚簇树中位置的唯一键,常为 id
回表 Table Lookup 二级索引拿到主键后,再查聚簇索引取完整行
覆盖索引 Covering Index 查询列全在索引内,无需回表
单列二级索引 Single-column Secondary Index 只包含一列的二级索引;多个条件时常只选其一

整理完毕,完结撒花~ 🌻

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐