子查询慢是因为被标记为DEPENDENT SUBQUERY,导致主表每行都重复执行;改用LEFT JOIN或EXISTS可显著提升性能,必要时用MATERIALIZED提示物化。

子查询为什么慢?先看执行计划里的“DEPENDENT SUBQUERY”
MySQL 遇到 WHERE 或 SELECT 中的非相关子查询(比如 (SELECT name FROM users WHERE id = orders.user_id)),如果优化器判断无法提前物化,就会标记为 DEPENDENT SUBQUERY —— 意思是:对主表每一行,都要重新执行一次子查询。10 万行订单?就执行 10 万次子查询。
用 EXPLAIN 看执行计划时,只要看到这一列出现 DEPENDENT SUBQUERY,基本就可以确认是性能瓶颈源头。
- 它无法利用主表索引下推,也不能被缓存复用
- 即使子查询本身只查单行,重复执行开销也极大(连接、解析、权限检查、临时表创建等)
- 在 MySQL 5.6–8.0 中,
IN (subquery)和= (subquery)的处理逻辑不同,后者更易触发依赖执行
把 WHERE 子查询改成 LEFT JOIN 是最稳的改写方式
多数场景下,WHERE ... IN (SELECT ...) 或 SELECT (SELECT ...) AS field 都能转成 JOIN。关键不是“能不能”,而是“怎么保语义不变”。
比如原语句:
SELECT o.id, o.total, (SELECT u.name FROM users u WHERE u.id = o.user_id) AS username FROM orders o;
应改写为:
SELECT o.id, o.total, u.name AS username FROM orders o LEFT JOIN users u ON u.id = o.user_id;
-
LEFT JOIN对应原SELECT (subquery)场景:用户可能不存在,需保留订单行(u.name为NULL) - 若子查询带
WHERE条件(如u.status = 'active'),必须写进ON而非WHERE,否则会退化为INNER JOIN语义 - 别名字段要显式声明,避免
USING引发歧义;ON条件里所有列都建议加表前缀
IN 子查询改写要注意 NULL 和去重
WHERE id IN (SELECT user_id FROM logs WHERE type = 'login') 这类常见过滤,直接 JOIN 容易多出重复行(一对多),导致结果膨胀或聚合错误。
- 优先用
EXISTS替代:WHERE EXISTS (SELECT 1 FROM logs l WHERE l.user_id = u.id AND l.type = 'login')—— MySQL 对EXISTS有半连接(semi-join)优化,通常比IN子查询快得多 - 若坚持用 JOIN,加
DISTINCT或GROUP BY成本高,不如在子查询里先GROUP BY user_id或用SELECT DISTINCT - 特别注意:如果子查询结果含
NULL,IN (..., NULL)整个条件返回UNKNOWN,结果为空;而EXISTS不受NULL影响
哪些子查询真没法 JOIN?得靠物化或提示
极少数情况确实绕不开子查询:比如窗口函数嵌套、子查询带 LIMIT、或需要动态计算 TOP-N(如“每个分类销量最高的商品”)。
- MySQL 8.0+ 支持
MATERIALIZED提示(/*+ MATERIALIZE() */),可强制将子查询结果物化为临时表,避免重复执行 - 对带
LIMIT的子查询,考虑用ROW_NUMBER()窗口函数重写,但注意排序字段必须有索引,否则性能更差 - 如果子查询只涉及单表且条件固定,建好覆盖索引后,有时
IN (val1, val2, ...)手动展开反而更快——但前提是值数量可控(
真正难优化的,往往是子查询里又嵌了子查询、还混着 OR 和函数调用。这种时候,先用 EXPLAIN FORMAT=TREE(8.0+)看优化器实际选择了哪条路径,比盲目改写更有效。



















