← 返回题目列表

什么是覆盖索引?和索引下推有什么区别?

高频 中等 第 1 / 28 题 更新于 2026/07/27
MySQL覆盖索引索引下推回表

简化版

覆盖索引是指查询需要的列都能从索引里拿到,不需要回表。索引下推是 MySQL 在扫描二级索引时,把部分 where 条件下推到存储引擎层先过滤,减少回表次数;它不等于不回表。

详细版

两者都和“减少回表”有关,但层次不同:

概念解决什么是否一定回表
覆盖索引查询列都在索引里,直接从索引返回结果不需要回表
索引下推(ICP)在索引扫描阶段先判断部分条件,过滤掉不满足的记录可能仍要回表

例子:

create index idx_name_age on user(name, age);

select name, age from user where name = 'Tom';

查询列 name, age 都在 idx_name_age 里,这是覆盖索引。

select * from user
where name like 'Tom%'
  and age = 18;

如果走 (name, age) 索引,存储引擎可以在索引里先判断 age = 18,减少回表,这就是索引下推。但因为 select * 需要其他列,最后满足条件的记录仍要回表。

完整版教学

一、先理解回表成本

InnoDB 的二级索引叶子节点保存的是索引列和主键值,不保存完整行。通过二级索引查到记录后,如果还要读取索引中没有的列,就要拿主键再查聚簇索引。

这个过程叫回表。少量回表没问题,但如果扫描大量二级索引记录再逐条回表,随机 I/O 和缓存压力都会上来。

假设 user 表有 100 万行,二级索引 idx_name(name) 找到 name like 'Tom%' 的候选记录有 5000 条。如果查询 select *,而其他列不在 idx_name 中,最坏情况下就要按 5000 个主键回到聚簇索引取完整行。即使这些页部分在 Buffer Pool 里,仍然会带来大量随机访问和 CPU 判断成本。

二级索引 idx_name:
name='Tom A' -> id=18
name='Tom B' -> id=203

聚簇索引 PRIMARY:
id=18  -> 完整行
id=203 -> 完整行

二级索引找到主键,再查主键树取完整行,这就是回表。

二、覆盖索引为什么快

覆盖索引的关键是:查询需要的列都在同一个索引中

create index idx_user_status_time on orders(user_id, status, create_time);

select status, create_time
from orders
where user_id = ?;

这个查询需要的 user_idstatuscreate_time 都在索引里,MySQL 不需要去聚簇索引取整行。执行计划里常见 Using index,表示可以只读索引完成查询。

覆盖索引常用于列表页、分页查询、报表统计。它不是让索引越宽越好,而是为高频查询建立刚好能覆盖的访问路径。

覆盖索引的判断对象不是“索引名字”,而是“这条查询所需列”。同一条索引对一条 SQL 可能是覆盖索引,对另一条 SQL 可能不是。比如 idx_user_status_time(user_id, status, create_time)select status, create_time ... 是覆盖的,但对 select status, amount ... 就不是,因为 amount 不在索引里。

查询是否覆盖原因
select user_id,status from orders where user_id=1过滤列和返回列都在索引中
select * from orders where user_id=1* 需要索引外的其他列
select count(*) from orders where user_id=1通常可覆盖只需要索引记录即可计数
select amount from orders where user_id=1取决于索引amount 不在索引里就要回表

易错点:覆盖索引不是一种单独的索引类型,而是一条查询刚好被某个索引“盖住”的执行效果。

三、索引下推解决的是“先过滤再回表”

索引下推全称 Index Condition Pushdown。没有 ICP 时,存储引擎可能先根据索引找到一批候选记录,再回表给 Server 层判断更多条件。有 ICP 时,能在索引层判断的条件会提前判断,不满足的记录不回表。

例如索引 (name, age)

select *
from user
where name like 'Tom%'
  and age = 18;

name like 'Tom%' 能定位一段索引范围,age = 18 虽然不一定能继续精准定位,但 age 在索引里,存储引擎可以扫描索引时先判断 age,过滤掉不满足的记录,减少回表。

用数字看更直观:假设 name like 'Tom%' 扫到 3000 条二级索引记录,其中只有 120 条 age=18。没有 ICP 时,可能先回表 3000 次,再在 Server 层过滤;有 ICP 时,存储引擎在索引记录里先判断 age,只把 120 条更可能满足条件的记录拿去回表。它省的是回表次数,不是让查询一定变成覆盖索引。

没有 ICP:
扫描索引 3000 条 -> 回表 3000 次 -> Server 过滤 age

有 ICP:
扫描索引 3000 条 -> 索引层过滤 age -> 回表约 120 次

四、覆盖索引和 ICP 的根本区别

覆盖索引是“最终结果不用回表”,因为所有列都在索引里。

索引下推是“回表前先过滤”,它减少回表次数,但只要查询还需要索引外的列,符合条件的记录仍要回表。

所以看到执行计划时要区分:

  • Using index:通常表示覆盖索引;
  • Using index condition:通常表示使用了索引下推;
  • 两者可能同时出现,但含义不同。

这两个概念所处阶段不同。覆盖索引看的是“返回结果能不能直接从索引拿”;ICP 看的是“扫描二级索引时,能不能把部分过滤条件提前交给存储引擎判断”。一个偏结果来源,一个偏过滤位置。很多候选人把 Using index condition 说成“不回表”,这就是典型混淆。

维度覆盖索引索引下推 ICP
核心目标避免回表减少回表
判断依据查询所需列是否都在索引里条件是否能在索引记录上提前判断
Explain 信号Using indexUsing index condition
是否依赖 select *select * 通常很难覆盖select * 也可能触发 ICP

五、使用覆盖索引的取舍

覆盖索引不是免费午餐。索引列越多,占用空间越大,写入、更新、删除时维护成本越高。设计时要问三个问题:

  • 这个查询是不是高频;
  • 返回列是不是稳定且较少;
  • 覆盖后能不能明显减少回表或排序。

如果只是偶发查询,不值得为了它建一个很宽的索引。

例如列表页固定展示 id、title、status、create_time,并按 user_id,status,create_time 查最近 20 条,那么索引 (user_id,status,create_time,id,title) 可能让列表查询完全走索引。但如果 title 很长,或者这个列表访问量不高,把大字段放进索引可能得不偿失。更常见的做法是覆盖轻量字段,把大文本、JSON、大 varchar 留给详情页回表读取。

设计覆盖索引时还要关注索引列顺序。前面的列仍要遵守过滤和排序的访问路径,不能为了覆盖返回列把低价值返回列放到最前面。覆盖是锦上添花,先有合理访问路径,再考虑是否补齐返回列。

六、执行计划该怎么读

看到 Explain 时,可以按下面顺序看:

1. key:实际用了哪个索引
2. rows:预计扫描多少索引记录或表记录
3. Extra=Using index:是否覆盖,能否不回表
4. Extra=Using index condition:是否 ICP,能否先过滤再回表
5. Extra=Using where:是否还有 Server 层条件判断

如果 Extra 只有 Using index condition,不要急着说“不回表”。要继续看查询列是否都在索引里。反过来,如果 Using index 出现,通常说明能用索引直接返回结果,但仍要结合 rows 判断扫描范围是否过大;覆盖索引扫 50 万行也不一定快。

七、常见误区与追问

  • 误区:覆盖索引是一种特殊的数据结构。 覆盖索引只是某个查询需要的列都能被某个索引满足,底层仍然是普通 B+ 树索引。
  • 误区:用了索引下推就一定不回表。 ICP 只是把能在索引记录上判断的条件提前过滤,查询需要索引外列时,最终满足条件的记录仍要回表。
  • 误区:select * 也容易形成覆盖索引。 除非表的所有列都在同一个索引中,否则 select * 通常需要回表;生产查询应避免无脑返回所有列。
  • 误区:覆盖索引越宽越好。 宽索引会增加存储、写入维护和缓存压力,只应服务高频且返回列稳定的查询。
  • 追问:Using indexUsing index condition 同时出现怎么解释? 可以理解为既存在索引层过滤,也可能有覆盖读取,但具体含义要结合查询列、where 条件和 MySQL 版本判断,不能只凭一个词下结论。
  • 追问:覆盖索引能解决所有慢查询吗? 不能;如果扫描范围太大、排序无法利用索引、统计信息不准或业务返回过多,覆盖索引也可能只是减少一部分回表成本。

八、加强记忆

覆盖索引回答的是“结果列从哪里拿”:列都在索引里,就不用回表。索引下推回答的是“条件在哪里过滤”:能在二级索引记录里判断的条件先交给存储引擎过滤,从而少回表。记住这两个动词:覆盖是“不回”,下推是“少回”;再结合 Explain 的 Using indexUsing index conditionrows 一起看,才不会把索引优化答成口号。