覆盖索引本身不提升子查询吞吐量,真正起作用的是用覆盖索引支撑的子查询+JOIN改写:子查询只SELECT主键且走覆盖索引,再通过INNER JOIN关联,避免临时表、回表和全表扫描。

覆盖索引本身不提升子查询吞吐量,真正起作用的是「用覆盖索引支撑的子查询 + JOIN 改写」——把原本低效的 IN (SELECT ...) 或相关子查询,替换成只查主键的子查询再关联。这能避免临时表、避免回表、避免全表扫描。
为什么 IN (SELECT ...) 在 MySQL 里容易变慢
MySQL 对 IN (SELECT ...) 的默认执行策略是物化(materialization):先执行子查询,结果存进无索引的内存/磁盘临时表,外层再逐行去这个临时表里找匹配。一旦子查询返回几千行,外层每行都要做一次线性查找,复杂度直接变成 O(N×M)。
- EXPLAIN 中看到
Using temporary或Using where; Using join buffer就是典型信号 - 哪怕子查询字段都有索引,只要它出现在
IN里,优化器大概率不会走semi-join优化(尤其在老版本或复杂 WHERE 条件下) -
EXISTS虽然语义等价,但 MySQL 5.7+ 后多数场景会自动转成 semi-join,比IN稍稳,但仍不如显式JOIN
子查询必须只 SELECT 主键,且走覆盖索引
子查询的目标不是“查出数据”,而是“精准定位 ID 列表”。所以它必须满足两个硬性条件:
- 子查询中
SELECT只能是单列:SELECT id,不能是SELECT *、SELECT id, status或任何多余字段,否则优化器放弃覆盖索引,触发回表 - 子查询的
WHERE和ORDER BY字段,必须被同一个复合索引完全覆盖,且顺序匹配:例如WHERE status = 1 ORDER BY created_at DESC,索引就得是INDEX(status, created_at, id)——id必须放最后,确保排序后直接输出主键,无需额外排序 - 执行计划中子查询那行的
Extra必须是Using index,而不是Using where; Using index(后者说明WHERE条件没被索引完全覆盖,仍要回表过滤)
外层必须用 INNER JOIN ON,不能用 WHERE IN 或 LEFT JOIN
JOIN 是物理连接动作,优化器可以分别走两张表的索引;而 WHERE t1.id IN (SELECT ...) 是逻辑包含判断,MySQL 很难对它做索引下推或批量匹配。
- 写法必须是:
SELECT t1.* FROM orders t1 INNER JOIN (SELECT id FROM orders WHERE ...) t2 ON t1.id = t2.id - 不能写成:
WHERE t1.id IN (SELECT id FROM ...),即使子查询很短,MySQL 5.7+ 仍可能退化为嵌套循环 - 也不能用
LEFT JOIN+IS NOT NULL模拟IN,这会让优化器误判驱动表,反而更慢 - 如果业务上允许丢弃无匹配的主表记录(即语义就是“有子集才取”),
INNER JOIN最安全;如需保留主表全部记录,应改用EXISTS并确保子查询有合适索引
最容易被忽略的索引结构陷阱
覆盖索引方案失效,90% 不是因为语法写错,而是索引和查询语义没咬合上。
-
ORDER BY字段不在索引最右位置:比如建了INDEX(created_at, status),但查询是WHERE status = 1 ORDER BY created_at,MySQL 无法跳过status做范围扫描,排序只能走Using filesort -
WHERE含非等值条件:status IN (1,2)或created_at > '2025-01-01',会导致后续字段无法用于排序,子查询被迫全索引扫描 + 内存排序 - 索引字段太多太宽:比如在覆盖索引里塞了 5 个
VARCHAR(255),B+ 树页变大,单页存的键值变少,树高增加,定位一个id反而多一次 IO —— 这时宁可拆成两个窄索引,也不要贪大求全


















