联合索引、覆盖索引与回表
联合索引把多列按确定顺序编码进同一棵 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. 等值列、范围列和排序共同决定列顺序
设计索引时,不能只背“区分度高的列放前面”。需要从主要查询形态出发:
- 用等值条件定位业务分区,例如 tenant 或 user。
- 再放常用等值过滤列。
- 范围列之后的列通常难以继续缩小单个连续扫描区间。
- 检查剩余列能否满足
ORDER BY,避免额外排序。
例如 (tenant_id, created_at, status) 能按 tenant 和时间扫描,但 created_at 是范围条件时,后面的 status 往往只能作为扫描后的过滤。若主要查询总是先按 tenant、status 等值过滤,再按时间翻页,(tenant_id, status, created_at) 更合适。
优化器也可能使用跳跃扫描、索引下推或多个索引合并,但这些能力不能替代按主要访问路径设计联合索引。
3. 二级索引叶子节点保存主键
InnoDB 聚簇索引的叶子节点保存整行数据。普通二级索引的叶子节点保存二级索引列和主键值。
使用二级索引查询不在索引中的列时,执行过程通常分两步:
- 从二级索引找到符合条件的主键。
- 根据主键访问聚簇索引,读取完整行。
第二步通常称为回表。若二级索引命中很多记录,随机或离散回表会成为主要成本。
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。
当查询需要的过滤列和返回列都包含在同一个索引中时,可以直接从索引返回,称为覆盖索引。可以通过调整联合索引列顺序和少量加入高频返回列减少回表,但索引越宽,空间、缓存和写入成本越高,所以应结合真实执行计划与查询频率决定。