Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6Mobile wallpaper 7Mobile wallpaper 8
2721 字
14 分钟
[MySQL] 索引:从页和 B+ 树到覆盖索引与回表
2026-08-12
2026-08-12

目录#

InnoDB 索引:从页和 B+ 树到覆盖索引与回表#

查一条数据时,最直接的办法是从头扫到尾。数据一多,这条路就慢了。索引做的事情很朴素:先把可以用来查找的值组织起来,再留下一条通往目标记录的路。

但这条路到底长什么样?为什么一说 InnoDB 索引,就会遇到页、B+ 树、聚簇索引、回表这些词?我们沿着一次查询走一遍,它们自然就连起来了。

一、索引先解决查找问题#

索引的核心作用,可以先记成一句话:高效查找数据。

没有合适索引时,数据库可能要检查很多行,才能找到符合条件的记录。有了索引,它可以先在已经排好结构的索引键中缩小范围,再去取目标数据。

比如成绩表经常按照学生和课程查询:

SELECT student_id, course_id, score
FROM score
WHERE student_id = 1
AND course_id = 1;

如果表上有 (student_id, course_id) 联合索引,数据库就多了一条可选的查找路径。不过,建了索引不代表优化器每次都会使用它。表有多大、条件能筛掉多少数据、查询要返回哪些列,都会影响最终选择。

索引也有代价。它会占用磁盘和缓存空间,执行 INSERTUPDATEDELETE 时还要维护相应的索引记录。所以索引要从真实查询出发,不能看见一列就建一个。

二、先看数据放在哪里#

(一)从 .ibd 文件看到页#

MySQL 8.0 默认开启独立表空间。一个 InnoDB 表在这种模式下会有自己的 .ibd 文件,表的数据和索引都放在里面。系统表空间、撤销表空间等采用其他文件形式,所以不能把所有 InnoDB 文件都概括成 .ibd

InnoDB 不会把这个文件当成一整块数据随意读写,它会把表空间划分成一个个页。页是 InnoDB 组织数据、把数据读入缓冲池以及刷回磁盘的基本单位,默认大小为 16 KB:

SHOW VARIABLES LIKE 'innodb_page_size';

Linux 常见文件系统块是 4 KB,但是数据库觉得 4 KB 太小,于是定义了 16 KB 页。这里把两个层次接到了一起。文件系统块由操作系统和文件系统管理,innodb_page_size 是 InnoDB 初始化实例时确定的配置;它的默认值恰好是 16 KB,还可以选择 4 KB、8 KB、32 KB 或 64 KB。

为什么一次读一页?因为页的内部地址是连续的,根据局部性原理,刚访问过的数据附近,往往还放着接下来可能访问的数据。把相邻记录一起读入缓冲池,后续查询就有机会直接从内存中取得,从而减少磁盘I/O开销,增加性能。

(二)页内查找#

树先帮我们定位到叶子页,进入页以后仍要寻找具体记录。原笔记把这一步记成“目录 + 主键”,这个抓法很直观。

页目录像一组稀疏路标。查找时先通过目录槽缩小范围,再到对应的记录分组中比较主键或索引键。这样一次定位就分成了两步:先沿索引树找到页,再在页内找到记录。

(三)残缺页#

如果一页还没完整写入磁盘就发生断电或进程退出,磁盘上可能只留下部分新内容,这叫不完整页写入。单靠“记录日志”四个字还说不清恢复过程。

InnoDB 会先把准备刷盘的页写入 doublewrite 区域,再写到表空间中的正式位置。崩溃恢复时,如果正式位置的页不完整,可以从 doublewrite 中找到完好的副本;redo log 再负责重放尚未写入数据文件的修改。两者处理的问题互相衔接。

三、B+ 树把许多页连成查找路径#

(一)页的连接#

一页只能容纳有限记录,数据继续增加时,就需要更多页。问题随之出现:我们怎样从许多页中快速找到目标页?

哈希适合等值查找,却不保留键的顺序;普通二叉搜索树能保持顺序,树高又可能随着数据量增长得太快。数据库更希望一个节点能指向很多子节点,让一层就排除大批无关页。多路平衡树正好符合这个要求。

MySQL 手册把 InnoDB 的常规索引结构称为 B-tree,课程中通常用 B+ 树来理解它:

  • 非叶子页保存分隔键和子页引用,用来指路;
  • 叶子页保存完整的索引记录;
  • 叶子页按照键的顺序相互连接,范围扫描可以继续向后走。

“只有叶子节点存储数据”这句话需要把“数据”说清楚。非叶子页也保存索引键和页引用,只是完整的索引记录位于叶子层。聚簇索引和二级索引的叶子记录还不一样,下一节继续区分。

(二)查找路径#

从根页开始,我们根据索引键选择子页,一层一层走到叶子页,再借助页目录找到具体索引记录。树的每个节点就是一个页,一个页能容纳许多导航项,所以树通常不需要长得很高。

这样看,B+ 树的作用就清楚了:它把许多页组织成一条层次稳定的查找路径,同时保留了键的顺序。

四、聚簇索引#

(一)几种常见索引#

索引含义
主键索引PRIMARY KEY 产生,键值唯一且不能为空
普通索引用于建立查找路径,不约束键值唯一,可以包含一列或多列
唯一索引UNIQUE 索引或约束产生,用来限制非 NULL 键值重复
全文索引基于文本列建立,配合全文检索语法搜索文本内容

主键索引、唯一索引描述的是约束和用途,聚簇索引描述的是行数据怎样和索引组织在一起。它们属于不同的观察角度。

(二)InnoDB 怎样选择聚簇索引#

每个 InnoDB 表都有一个聚簇索引,它的叶子记录保存行数据。选择顺序如下:

  1. 表中定义了 PRIMARY KEY,主键就是聚簇索引。
  2. 没有主键,InnoDB 选择第一个“所有键列都定义为 NOT NULL”的 UNIQUE 索引。
  3. 前两项都不存在,InnoDB 创建隐藏的 GEN_CLUST_INDEX,用内部生成的 6 字节行 ID 组织记录。

原笔记写的是“第一个 UNIQUENOT NULL 字段”,容易让人理解成随便找一个唯一字段。准确条件落在整个唯一索引上:它的每一个键列都必须是 NOT NULL

隐藏行 ID 也不会变成应用程序可以直接查询的业务列。实际建表时,主动设计简短、稳定的主键,会让表结构更清楚;主键值还会进入二级索引,主键过长会跟着放大多个索引。

五、二级索引#

(一)叶子记录#

聚簇索引以外的 InnoDB 索引都叫二级索引。假设 score 表以 id 为主键,并建立下面的联合索引:

CREATE INDEX idx_score_student_course
ON score (student_id, course_id);

这个二级索引的叶子记录可以理解成:

student_id + course_id + 主键 id

二级索引不重复保存整行,它保存自己的索引键和对应行的主键。这样既能按照 student_idcourse_id 查找,也能在需要完整记录时继续找到聚簇索引。

(二)回表#

现在查询索引之外的 score 列:

SELECT student_id, course_id, score
FROM score
WHERE student_id = 1
AND course_id = 1;

按照这条二级索引访问路径,查找过程是:

  1. 先在 idx_score_student_course 中找到符合条件的记录;
  2. 从二级索引记录中取得主键 id
  3. 再用 id 查询聚簇索引,取出包含 score 的完整行。

原笔记里的“先通过二级索引找到主键,再通过聚簇索引获得完整记录”,说的就是回表。可以把它理解成先查到一张门牌号,再按照门牌号去主表中取完整信息。

六、索引覆盖#

那么,如果查询需要的列本来就包含在索引记录中呢?

SELECT student_id, course_id
FROM score
WHERE student_id = 1
AND course_id = 1;

二级索引中已经有 student_idcourse_id。如果优化器选择它,就可以直接返回结果,无需根据主键再访问聚簇索引。这种“一个索引包含了某条查询所需信息”的关系,就叫覆盖索引。

覆盖索引不是额外创建出来的一类索引。同一个 (student_id, course_id) 索引,可以覆盖上面的查询;当查询列表增加 score 后,它又需要回表。判断是否覆盖,要同时看索引保存的列和这条查询真正需要的列。

减少回表通常能少一次聚簇索引访问,但也不能为了覆盖所有查询就不断往索引里加列。索引越宽,占用的空间越多,写入时维护它的成本也越高。

七、把完整做一遍#

(一)创建并查看索引#

score(student_id, course_id) 上创建普通联合索引:

CREATE INDEX idx_score_student_course
ON score (student_id, course_id);

创建后,用 SHOW INDEX 查看索引定义:

SHOW INDEX FROM score
WHERE Key_name = 'idx_score_student_course';

实际结果返回两行:Seq_in_index = 1 对应 student_idSeq_in_index = 2 对应 course_idNon_unique = 1 表示这是普通的非唯一索引。两列共同组成一个索引,列顺序也是索引定义的一部分。

SHOW INDEX 只能证明索引已经创建,并展示它的元数据。某条查询最终有没有使用它,还要查看执行计划。本次练习没有运行 EXPLAIN,所以这里只记录已经验证过的创建、查看和删除结果。

(二)删除并确认结果#

DROP INDEX idx_score_student_course ON score;

删除后再次执行带 Key_name 条件的 SHOW INDEX,实际结果为 0 行,说明这条索引已经不存在。

索引适合放在经常参与 WHEREJOIN 或排序的列上,也要结合区分度、表规模和读写比例判断。索引能缩小检索范围,覆盖查询还能少走一次回表;代价是额外空间,以及每次写入时的维护工作。

[MySQL] 索引:从页和 B+ 树到覆盖索引与回表
https://blog.csdn.net/2501_93882415/article/details/163705320
作者
Mem0rin
发布于
2026-08-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时