跳到主要内容

联合索引、覆盖索引与回表

联合索引把多列按确定顺序编码进同一棵 B+ 树。索引能否缩小扫描范围、是否需要回表,以及能否直接提供排序,都取决于查询条件如何使用这组有序键。

1. 联合索引先按第一列排序

假设表上有索引 (tenant_id, status, created_at),索引项先按 tenant_id 排序;tenant 相同时再按 status 排序;前两列都相同时,created_at 才是有序的。

因此,以下条件可以直接利用连续索引范围:

WHERE tenant_id = 10
AND status = 'PAID'
AND created_at >= '2026-08-01'

如果只给出 status,不同 tenant 的同一状态分散在整棵索引中,通常无法通过这棵索引快速定位一个连续区间。这就是联合索引的左前缀特性。

2. 等值列、范围列和排序共同决定列顺序

设计索引时,不能只背“区分度高的列放前面”。需要从主要查询形态出发:

  1. 用等值条件定位业务分区,例如 tenant 或 user。
  2. 再放常用等值过滤列。
  3. 范围列之后的列通常难以继续缩小单个连续扫描区间。
  4. 检查剩余列能否满足 ORDER BY,避免额外排序。

例如 (tenant_id, created_at, status) 能按 tenant 和时间扫描,但 created_at 是范围条件时,后面的 status 往往只能作为扫描后的过滤。若主要查询总是先按 tenant、status 等值过滤,再按时间翻页,(tenant_id, status, created_at) 更合适。

优化器也可能使用跳跃扫描、索引下推或多个索引合并,但这些能力不能替代按主要访问路径设计联合索引。

3. 二级索引叶子节点保存主键

InnoDB 聚簇索引的叶子节点保存整行数据。普通二级索引的叶子节点保存二级索引列和主键值。

使用二级索引查询不在索引中的列时,执行过程通常分两步:

  1. 从二级索引找到符合条件的主键。
  2. 根据主键访问聚簇索引,读取完整行。

第二步通常称为回表。若二级索引命中很多记录,随机或离散回表会成为主要成本。

4. 覆盖索引让查询直接从索引返回

查询需要的过滤列和返回列都能从同一个索引得到时,该索引覆盖了这次查询:

SELECT created_at
FROM orders
WHERE tenant_id = 10 AND status = 'PAID';

索引 (tenant_id, status, created_at) 已包含查询所需数据,不必再访问聚簇索引。EXPLAIN 的 Extra 中可能出现 Using index

覆盖是索引与具体查询之间的关系,不是某个索引永久具有的属性。把所有返回列都塞进索引会增加空间、缓存压力和写放大,应优先覆盖高频且性能敏感的窄查询。

5. 主键宽度会影响全部二级索引

InnoDB 的二级索引项需要携带主键。主键很宽时,每棵二级索引都会变大,单个页能容纳的记录减少,缓存命中率和写入成本也会受到影响。

这也是主键通常选择稳定、较短字段的原因之一。业务唯一标识可以建立唯一索引,不必一定充当聚簇主键。

6. SQL 使用索引仍然可能很慢

出现 key 不为空,只说明优化器选择了某个索引。仍需检查:

  • type 和实际扫描行数。
  • key_len 反映使用到哪些索引部分。
  • 过滤后剩余行数与返回行数。
  • 是否大量回表。
  • 是否出现额外排序或临时表。
  • 统计信息与真实数据分布是否一致。
  • 锁等待、I/O 和缓冲池命中情况。

MySQL 8 可以用 EXPLAIN ANALYZE 查看实际行数、循环次数和各算子耗时。优化应以执行证据为准,不只根据 SQL 文字猜测。

7. 常见问题

7.1 LIKE 'abc%' 会破坏左前缀吗

它通常可以利用字符串前缀形成范围扫描,但后续索引列是否还能继续缩小范围,要看优化器生成的访问区间。LIKE '%abc' 无法从 B+ 树起点定位前缀,通常不能用普通索引完成有效范围查找。

7.2 联合索引是否能替代所有单列索引

不能。联合索引只能支持与其有序前缀匹配的访问路径。如果某列经常独立查询,且不能依靠现有索引有效定位,仍可能需要单列索引或另一组联合索引。最终要结合读写成本和真实查询频率取舍。

8. 面试题

8.1 什么是覆盖索引和回表,如何减少回表成本

出现公司:字节跳动

考察重点

  • InnoDB 聚簇索引与二级索引的叶子内容。
  • 覆盖索引是针对具体查询的判断。
  • 回表次数、索引宽度和写入成本的权衡。

相关内容:第 3 节“二级索引叶子节点保存主键”至第 6 节“SQL 使用索引仍然可能很慢”。

参考回答

InnoDB 二级索引叶子节点保存索引列和主键。查询二级索引没有包含的列时,要先取得主键,再访问聚簇索引读取整行,这一步是回表。若过滤条件命中大量记录,大量回表会明显增加 I/O。

当查询需要的过滤列和返回列都包含在同一个索引中时,可以直接从索引返回,称为覆盖索引。可以通过调整联合索引列顺序和少量加入高频返回列减少回表,但索引越宽,空间、缓存和写入成本越高,所以应结合真实执行计划与查询频率决定。