目录
InnoDB 索引:从页和 B+ 树到覆盖索引与回表
查一条数据时,最直接的办法是从头扫到尾。数据一多,这条路就慢了。索引做的事情很朴素:先把可以用来查找的值组织起来,再留下一条通往目标记录的路。
但这条路到底长什么样?为什么一说 InnoDB 索引,就会遇到页、B+ 树、聚簇索引、回表这些词?我们沿着一次查询走一遍,它们自然就连起来了。
一、索引先解决查找问题
索引的核心作用,可以先记成一句话:高效查找数据。
没有合适索引时,数据库可能要检查很多行,才能找到符合条件的记录。有了索引,它可以先在已经排好结构的索引键中缩小范围,再去取目标数据。
比如成绩表经常按照学生和课程查询:
SELECT student_id, course_id, scoreFROM scoreWHERE student_id = 1 AND course_id = 1;如果表上有 (student_id, course_id) 联合索引,数据库就多了一条可选的查找路径。不过,建了索引不代表优化器每次都会使用它。表有多大、条件能筛掉多少数据、查询要返回哪些列,都会影响最终选择。
索引也有代价。它会占用磁盘和缓存空间,执行 INSERT、UPDATE、DELETE 时还要维护相应的索引记录。所以索引要从真实查询出发,不能看见一列就建一个。
二、先看数据放在哪里
(一)从 .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 表都有一个聚簇索引,它的叶子记录保存行数据。选择顺序如下:
- 表中定义了
PRIMARY KEY,主键就是聚簇索引。 - 没有主键,InnoDB 选择第一个“所有键列都定义为
NOT NULL”的UNIQUE索引。 - 前两项都不存在,InnoDB 创建隐藏的
GEN_CLUST_INDEX,用内部生成的 6 字节行 ID 组织记录。
原笔记写的是“第一个 UNIQUE 和 NOT NULL 字段”,容易让人理解成随便找一个唯一字段。准确条件落在整个唯一索引上:它的每一个键列都必须是 NOT NULL。
隐藏行 ID 也不会变成应用程序可以直接查询的业务列。实际建表时,主动设计简短、稳定的主键,会让表结构更清楚;主键值还会进入二级索引,主键过长会跟着放大多个索引。
五、二级索引
(一)叶子记录
聚簇索引以外的 InnoDB 索引都叫二级索引。假设 score 表以 id 为主键,并建立下面的联合索引:
CREATE INDEX idx_score_student_courseON score (student_id, course_id);这个二级索引的叶子记录可以理解成:
student_id + course_id + 主键 id二级索引不重复保存整行,它保存自己的索引键和对应行的主键。这样既能按照 student_id、course_id 查找,也能在需要完整记录时继续找到聚簇索引。
(二)回表
现在查询索引之外的 score 列:
SELECT student_id, course_id, scoreFROM scoreWHERE student_id = 1 AND course_id = 1;按照这条二级索引访问路径,查找过程是:
- 先在
idx_score_student_course中找到符合条件的记录; - 从二级索引记录中取得主键
id; - 再用
id查询聚簇索引,取出包含score的完整行。
原笔记里的“先通过二级索引找到主键,再通过聚簇索引获得完整记录”,说的就是回表。可以把它理解成先查到一张门牌号,再按照门牌号去主表中取完整信息。
六、索引覆盖
那么,如果查询需要的列本来就包含在索引记录中呢?
SELECT student_id, course_idFROM scoreWHERE student_id = 1 AND course_id = 1;二级索引中已经有 student_id 和 course_id。如果优化器选择它,就可以直接返回结果,无需根据主键再访问聚簇索引。这种“一个索引包含了某条查询所需信息”的关系,就叫覆盖索引。
覆盖索引不是额外创建出来的一类索引。同一个 (student_id, course_id) 索引,可以覆盖上面的查询;当查询列表增加 score 后,它又需要回表。判断是否覆盖,要同时看索引保存的列和这条查询真正需要的列。
减少回表通常能少一次聚簇索引访问,但也不能为了覆盖所有查询就不断往索引里加列。索引越宽,占用的空间越多,写入时维护它的成本也越高。
七、把完整做一遍
(一)创建并查看索引
在 score(student_id, course_id) 上创建普通联合索引:
CREATE INDEX idx_score_student_courseON score (student_id, course_id);创建后,用 SHOW INDEX 查看索引定义:
SHOW INDEX FROM scoreWHERE Key_name = 'idx_score_student_course';实际结果返回两行:Seq_in_index = 1 对应 student_id,Seq_in_index = 2 对应 course_id,Non_unique = 1 表示这是普通的非唯一索引。两列共同组成一个索引,列顺序也是索引定义的一部分。
SHOW INDEX 只能证明索引已经创建,并展示它的元数据。某条查询最终有没有使用它,还要查看执行计划。本次练习没有运行 EXPLAIN,所以这里只记录已经验证过的创建、查看和删除结果。
(二)删除并确认结果
DROP INDEX idx_score_student_course ON score;删除后再次执行带 Key_name 条件的 SHOW INDEX,实际结果为 0 行,说明这条索引已经不存在。
索引适合放在经常参与 WHERE、JOIN 或排序的列上,也要结合区分度、表规模和读写比例判断。索引能缩小检索范围,覆盖查询还能少走一次回表;代价是额外空间,以及每次写入时的维护工作。
部分信息可能已经过时











