相关子查询比JOIN慢是因为每处理一行外层数据都要重新执行一次子查询,导致N×M级计算;而JOIN通常只需一次哈希构建和一次扫描。
因为每次外层行扫描时,WHERE 或 SELECT 中的子查询都要重新执行一次,数据量大时会形成 N×M 级计算。比如外层 10 万行、内层平均查 100 行,实际执行就是 1000 万次逻辑读——而等价的 JOIN 通常只需一次哈希构建 + 一次扫描。
常见错误现象:EXPLAIN 显示子查询被标记为 DEPENDENT SUBQUERY,且 rows 列数值随外层行数线性增长;在 MySQL 5.7+ 或 PostgreSQL 中,EXPLAIN ANALYZE 能直接看到子查询被调用次数。
optimizer_switch='semijoin=on,materialization=on'
IN 和 EXISTS 相关子查询优化较好,但 SELECT (SELECT ...) 这类标量子查询仍大概率走嵌套循环TOP 就失效不是所有情况都能用 JOIN 替代。典型硬需求包括:
SELECT id, (SELECT COUNT(*) FROM logs l WHERE l.user_id = u.id AND l.created_at > u.last_login) AS recent_log_count FROM users u
WHERE EXISTS (SELECT 1 FROM acl WHERE acl.user_id = u.id AND acl.resource = 'report' AND acl.level >= 3)),此时 EXISTS 是语义必需,但可考虑物化权限视图缓解注意: IN 子句含空值时行为与 EX 不同,别盲目替换;
NOT IN (subquery) 遇到 NULL 会整个返回空结果集,这是语义陷阱,不是性能问题。
把子查询从“每行一次”变成“整体一次”,核心是提前聚合或去重。关键不是换语法,而是识别可提取的公共计算逻辑。
例如原查询:
SELECT u.name, (SELECT MAX(o.amount) FROM orders o WHERE o.user_id = u.id) AS max_order FROM users u;
可改写为:
SELECT u.name, agg.max_order FROM users u LEFT JOIN ( SELECT user_id, MAX(amount) AS max_order FROM orders GROUP BY user_id ) agg ON agg.user_id = u.id;
WHERE
WITH CTE,但注意:非递归 CTE 在 12+ 版本默认被物化(即执行一次),而 MySQL 8.0 的 CTE 默认不物化,需加 /*+ MATERIALIZE */ 提示WHERE user_id IN (SELECT DISTINCT user_id FROM orders) 缩小外层范围相关子查询的索引有效性极度依赖谓词结构。只建 (user_id) 单列索引往往不够,因为优化器仍需回表取值或过滤其他字段。
SELECT (SELECT col FROM t WHERE t.a = u.x AND t.b > 10 ORDER BY t.c DESC LIMIT 1),理想索引是 (a, b, c) —— 把关联字段放最左,过滤字段居中,排序/返回字段放右GROUP BY 或 HAVING,单列索引基本无效,必须覆盖所有分组和过滤字段LOWER(email),可建 CREATE INDEX idx_users_lower_email ON users (LOWER(email))
最容易被忽略的一点:相关子查询中的字段如果来自外层表的函数结果(如 DATE(created_at)),即使内层有索引也大概率失效——先确认是否能把计算移到外层或预计算列上。