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