IN子查询变慢因MySQL 5.6前转为嵌套循环,5.7+需满足无GROUP BY等条件才启用半连接优化;应改用EXISTS、确保索引或缓存结果。

子查询里用 IN 为什么突然变慢?
因为 MySQL 5.6 之前,IN (SELECT ...) 会被转成嵌套循环(NLJ),外层每行都执行一次子查询,等价于全表扫描子查询表。5.7+ 虽支持半连接优化(semi-join),但前提是子查询不能含 GROUP BY、DISTINCT、聚合函数或相关子查询。
- 检查执行计划:看到
type: ALL或Extra: Using where; Using join buffer就是危险信号 - 把
IN (SELECT id FROM t2 WHERE ...)改成EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id AND ...),让优化器更可能走索引 - 如果必须用
IN,确保子查询结果集小且t2.id有索引——否则先用临时表缓存结果再JOIN
WHERE 子句里写相关子查询,索引还有效吗?
不一定。相关子查询(correlated subquery)每次都要重新计算,如果子查询里没用上外层字段的索引条件,就容易退化成对子表的重复全扫。
- 典型陷阱:
SELECT * FROM orders WHERE status = 'paid' AND user_id IN (SELECT id FROM users WHERE region = 'CN')—— 这里users表没被orders驱动,region索引可能被忽略 - 改写优先级:能
JOIN就别用相关子查询;JOIN后加WHERE过滤,比在子查询里过滤更易命中索引 - 如果非用不可,确保子查询中用于关联的字段(如
users.id)和过滤字段(如users.region)都有联合索引,比如INDEX(region, id)
子查询返回多列时,SELECT * 会拖垮性能?
会。尤其是子查询作为派生表(derived table)出现在 FROM 子句时,MySQL 会物化结果到临时表,默认用 MyISAM(旧版)或 InnoDB(8.0+),没索引、不压缩、全字段存储。
- 只选需要的列:
SELECT a.id, a.name FROM (SELECT id, name FROM users WHERE active = 1) AS a比SELECT * FROM (SELECT * FROM users ...)少读字段、少内存、更容易走覆盖索引 - 如果子查询本身能走覆盖索引(例如
SELECT id, email FROM users WHERE status=1,且INDEX(status, id, email)存在),物化开销会大幅降低 - 8.0+ 可考虑用 CTE +
MATERIALIZED提示(需 optimizer_switch 启用),但不如直接改写为JOIN可控
LIMIT 和子查询一起用,为什么索引失效?
因为 LIMIT 在子查询里生效范围有限——它只限制子查询输出行数,不改变其扫描方式。优化器仍可能选择全表扫描后再取前 N 行。
- 错误写法:
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users ORDER BY created_at DESC LIMIT 10)——users表仍可能全扫 - 正确思路:先把子查询结果落地,比如用
CREATE TEMPORARY TABLE tmp_users AS SELECT id FROM users ORDER BY created_at DESC LIMIT 10,再JOIN;或者用JOIN+LIMIT外推(需业务允许近似结果) - 注意:MySQL 不支持子查询中对非主键字段
ORDER BY ... LIMIT的索引下推,除非该字段在索引最左前缀上
最常被忽略的一点:子查询是否“可展开”(subquery flattening)取决于优化器成本估算,而估算依赖统计信息准确度。表数据量变化后没 ANALYZE TABLE,即使写了正确结构,也可能走错执行计划。

















