在 MySQL 5.7 中,子查询索引失效是一个容易踩中的性能陷阱:子查询语法写起来很顺手,但实际执行时外层索引往往无法正常使用。问题的根源在于优化器对相关子查询的处理策略——它不会先将子查询结果物化再匹配,而是采用“逐行驱动”方式:主表每扫描一行,子查询就会被重复执行一次。这样一来,外层的 WHERE 条件根本无法利用索引定位,EXPLAIN 中如果出现 DEPENDENT SUBQUERY,基本就等于明确告诉你——索引已经失效。

MySQL 5.7 子查询索引失效的根本原因在于,优化器对非物化相关子查询采用“逐行驱动”策略,而非先物化再匹配,导致外层表索引无法生效。
EXPLAIN 出现 DEPENDENT SUBQUERY 即表明索引失效
一旦子查询中引用了外层表的字段(例如 u.id = o.user_id),MySQL 5.7 通常不会进行物化,而是直接将其标记为 DEPENDENT SUBQUERY。这意味着什么?主表每扫描一行,子查询就跟着执行一次。外层的 WHERE 条件形同虚设——索引定位无法使用,只能全表扫描或索引全扫描。执行计划中经常出现 type: ALL 或 Extra: Using where; Using join buffer,这些信号都表明性能已经严重下降。
IN/=/NOT IN 子查询让外层索引“形同虚设”
这类子查询在 MySQL 5.7 中几乎必然触发“先执行后匹配”的流程。优化器会先执行 SELECT user_id FROM logs WHERE status = 1,很可能全表扫描得到一个临时结果集,然后再用这个结果集去匹配主表。此时即使主表的 id 字段有索引,也无法发挥作用——因为匹配逻辑已经变成“遍历临时表”,而不是“走索引定位”。就算子查询本身能用到索引,外层表依然被迫执行 ALL 扫描。更糟糕的是 NOT IN 还会受到 NULL 值的影响:只要子查询返回任意一个 NULL,整个条件就恒为 FALSE,结果直接变成空集。
JOIN 能下推条件、复用索引、避免重复计算
将子查询改写为 JOIN 后,优化器就能够统一规划执行路径。例如,logs.status = 1 这个条件可以直接下推到 logs 表扫描时过滤,减少中间结果集。如果 orders.user_id 和 logs.user_id 都有索引,还可以使用 Index Nested-Loop Join 快速匹配。这样就不会为每一行订单重复打开、解析、执行子查询逻辑,也无需创建和销毁临时表。尤其要注意,子查询中如果带有 ORDER BY RAND() 或 GROUP BY,很容易导致物化失败,而 JOIN 可以完全绕过这个坑。
不是所有子查询都能安全 JOIN 化
强行将子查询替换为 JOIN 也可能引发问题。改写之前必须核对三件事:第一,连接字段是否都有索引——orders.user_id 和 users.id 缺一不可,否则 JOIN 反而更慢。第二,NULL 值如何处理——LEFT JOIN ... IS NULL 和 NOT EXISTS 并不等价,如果右表连接列允许 NULL,需要补上 AND right.id IS NOT NULL 才行。第三,行数是否会膨胀——一对多关系(例如订单→订单项)直接 JOIN 会导致主表行重复,必须使用 DISTINCT 或改用 EXISTS 来避免。
真正拖慢性能的,往往不是语法本身,而是没有看清子查询是否真的“相关”、是否存在隐式聚合、连接字段是否允许为空。这些细节只要漏掉一个,JOIN 改写后的结果可能出错,速度也可能更差。调优这件事,说到底就是抠细节。
