相关子查询执行次数等于外层查询返回的行数,因其每行触发一次子查询;非相关子查询仅执行一次并复用结果。

相关子查询慢,是因为它被外层每一行触发一次执行,形成 N×M 级扫描;而非相关子查询只执行一次,数据库可物化结果复用。
相关子查询到底执行了多少次?
关键看它是否引用外层表别名(如 e.department_id、u.id)。只要子查询里出现这种引用,优化器就无法提前计算,只能为外层每一行重新跑一遍内层逻辑。
- 外层 1000 行 + 子查询无索引 → 可能触发 1000 次全表扫描
- 外层 5 万用户 +
(SELECT COUNT(*) FROM orders WHERE user_id = u.id)→ 最坏情况扫描 orders 表 5 万次 - EXPLAIN 中若看到
DEPENDENT SUBQUERY(MySQL)或Correlated Subquery(PostgreSQL),就是明确信号
非相关子查询为什么快?
它不依赖外部数据,优化器可在主查询前独立执行并缓存结果。例如 (SELECT AVG(salary) FROM employees),无论外层有多少行,这个平均值只算一次。
- 执行计划中通常显示为
InitPlan(PostgreSQL)或单独的select_type: SUBQUERY(MySQL) - 即使子查询本身慢(比如聚合大表),也只拖慢整体一次,不会放大
- 数据库可能将其“上拉”(pull up)融入主查询树,进一步优化连接顺序
为什么不能靠优化器自动改写?
不是所有相关子查询都能安全转成 JOIN —— 语义、NULL 处理、重复行控制都可能出错。
-
WHERE id IN (SELECT user_id FROM orders)改JOIN后,orders 有 3 条记录就会让同一用户出现 3 次,需加DISTINCT或GROUP BY -
WHERE salary > (SELECT AVG(salary) FROM e2 WHERE e2.dept = e1.dept)改 JOIN 必须先聚合(GROUP BY dept),再关联,否则结果错 -
NOT IN (SELECT ...)若子查询含 NULL,逻辑等价于永假;但LEFT JOIN ... WHERE right.id IS NULL要求right.id非 NULL,否则漏数据
最容易被忽略的执行时机问题
相关子查询的执行时机不是“统一预计算”,而是随外层行逐个触发 —— 这意味着:索引是否生效、是否走临时表、甚至事务隔离级别下的快照可见性,都会逐行变化。
- 外层某行在 RC 隔离级下读到新插入的订单,子查询却因快照未包含该行而查不到,结果不一致
- 子查询里用了
NOW()或UUID(),每次执行返回不同值,导致不可预测行为 - 如果外层用了
LIMIT 10,但子查询仍对全部匹配行执行(优化器未下推 limit),性能浪费严重










