覆盖索引能避免回表,因二级索引含主键值,当select字段全在索引中时可直接返回数据,无需回聚簇索引查整行;explain中extra为using index即命中覆盖索引。

覆盖索引为什么能避免回表
MySQL 的二级索引(非聚簇索引)只存储索引列 + 主键值,不存整行数据。当 SELECT 的所有字段都包含在某个索引中时,优化器可以直接从该索引页拿到全部所需数据,无需再用主键去聚簇索引里查一遍——这个“再去主键索引找整行”的过程就叫回表。覆盖索引本质是让查询“止步于二级索引”,跳过回表开销。
如何判断一个查询是否走覆盖索引
用 EXPLAIN 看 Extra 列是否含 Using index(注意不是 Using index condition):
EXPLAIN SELECT user_id, status FROM orders WHERE status = 'paid';
如果 status 字段上有联合索引 (status, user_id),就会命中覆盖索引;但如果只查 user_id, status, created_at,而 created_at 不在索引里,Extra 就会变成 Using where; Using index 或干脆没有 Using index,说明要回表。
-
Using index✅ 表示纯覆盖索引扫描 -
Using index condition❌ 表示用了 ICP(索引条件下推),但未必覆盖 -
Using where+ 没有Using index❌ 基本确认要回表
设计覆盖索引的实操要点
覆盖索引不是越多越好,得按高频查询反向构建,且要注意顺序和冗余:
- 把
WHERE条件字段放前面(满足最左前缀) - 把
SELECT中的其他字段追加在后面(尤其是ORDER BY和GROUP BY字段) - 避免把大字段(如
TEXT、长VARCHAR)放进索引——会显著增大索引体积,降低缓存效率 - 如果已有索引
(a, b),又新增查询常查a, b, c,优先考虑扩展为(a, b, c),而不是新建(a, b, c)索引(否则(a, b)可能被废弃)
例如:常见分页查询 SELECT id, title, status FROM article WHERE status = ? ORDER BY create_time DESC LIMIT 20,更适合建 (status, create_time, id, title) 而非 (status, create_time) —— 后者仍需回表取 id 和 title。
覆盖索引的隐性代价与陷阱
它省了回表,但可能带来别的负担:
- 索引变宽 → 更多磁盘 IO、更少页缓存命中率 →
SELECT *场景下反而可能比窄索引+回表还慢 - 写入性能下降:每条
INSERT/UPDATE都要维护更多索引字段 -
UPDATE涉及覆盖索引中的任意字段时,该索引页必须更新(哪怕只改了没出现在索引里的列,只要该行被修改,且索引含其主键,就仍需更新索引中的主键副本) - JSON、Generated Column 等特殊类型字段若参与覆盖,需确认它们是否真正被索引存储(比如虚拟生成列必须显式
STORED才能进索引)
真正难的不是建覆盖索引,而是权衡:哪些查询值得为它增加写开销和存储?哪些字段看似“顺手加进去”,实则让索引膨胀 3 倍?线上慢查分析时,先看 EXPLAIN 是否真触发了 Using index,再看 Handler_read_index 和 Handler_read_next 的增长是否合理——别让“以为覆盖”变成“实际更慢”。











