Dependent Subquery 慢是因为需为外部查询每行重复执行,呈循环嵌套;可改写为 LEFT JOIN 的条件包括:子查询返回单值、仅含等值关联、无复杂逻辑及 GROUP BY 后过滤,并注意 NULL 处理、预聚合防重复、别名规范和语义一致性。
Dependent Subquery 为什么慢
navicat 执行计划里标出 dependent subquery,说明这个子查询要为外部查询的每一行重新执行一次,且依赖外部行的值(比如用到了 t1.id)。它本质是“循环嵌套”,数据量稍大就指数级拖慢——哪怕子查询本身很快,10 万行外部结果 × 每次 10ms = 16 分钟。
怎么识别能改写成 JOIN 的子查询
不是所有子查询都能安全改写,重点看是否满足以下条件:
- 子查询出现在
SELECT列表或WHERE中,且只返回单个值(MAX()、COUNT(*)、id等) - 子查询的
WHERE条件只含一个等值关联(如WHERE t2.t1_id = t1.id),没有OR、IN、IS NULL等复杂逻辑 - 子查询不带
GROUP BY或聚合后还需过滤(否则需用派生表或 CTE) - 你接受结果中可能因一对多关系产生重复行(JOIN 天然行为),或已用
DISTINCT/GROUP BY处理
改写步骤:从 SELECT 子查询到 LEFT JOIN
典型场景:查用户姓名 + 其最新订单时间。原 SQL 常这么写:
SELECT u.name, (SELECT MAX(o.create_time) FROM orders o WHERE o.user_id = u.id) AS last_order_time FROM users u;
改写要点:
- 把子查询拆出来,作为
LEFT JOIN的右表(用GROUP BY预聚合,避免笛卡尔积) - 关联字段必须明确,JOIN 条件和原子查询 WHERE 中的等值条件一致
- SELECT 列中原来子查询的结果,改成取 JOIN 表的聚合字段
改写后:
SELECT u.name, o.last_time AS last_order_time FROM users u LEFT JOIN ( SELECT user_id, MAX(create_time) AS last_time FROM orders GROUP BY user_id ) o ON o.user_id = u.id;
常见翻车点和绕过方法
实际改写时容易掉坑:
-
NULL值导致 JOIN 失效:如果user_id允许为NULL,子查询中GROUP BY user_id会把所有NULL归为一组,JOIN 时无法匹配。解决:提前WHERE user_id IS NOT NULL过滤,或用COALESCE(user_id, -1)统一处理 - 一对多引发重复行:若没预聚合,直接
JOIN orders会导致一个用户对应多行订单,SELECT u.name被撑开。务必先聚合再 JOIN,别图省事少写GROUP BY - MySQL 5.7 以下版本不支持 FROM 子句中无别名的子查询:记得给内层子查询加别名(如
) o),否则报错Every derived table must have its own alias - 原语句有
ORDER BY+LIMIT:子查询里用LIMIT不能直接搬进 JOIN,得用窗口函数(MySQL 8.0+)或变量模拟 top-N
真正卡住的往往不是语法,而是业务语义是否等价——比如“最新订单”在并发插入下可能有微小偏差,而子查询每次重新算,JOIN 方案却是快照式聚合。这点容易被忽略,但线上一致性要求高时必须确认。











