子查询在WHERE中并非先算完再过滤,而是依类型由优化器决定是否物化或转为JOIN;标量子查询每行触发、IN可能转半连接、EXISTS可短路;相关子查询依赖外层字段致执行次数倍增。

子查询在 WHERE 中的执行时机:不是“先算完再过滤”
很多人以为 WHERE 里的子查询会先完整执行一遍、生成结果集,再拿去和外层逐行比对。实际并非如此——优化器会根据子查询类型决定是否物化(materialize),甚至可能重写为连接(JOIN)。
- 标量子查询(返回单值)通常被优化为“每行触发一次”,如果没加索引,外层每查一行都可能触发一次子查询,性能雪崩
-
IN (SELECT ...)在 MySQL 5.6+ 和 PostgreSQL 中大概率被转成半连接(semi-join),但 SQLite 或旧版 MySQL 可能真会全量执行子查询再哈希匹配 - 用
EXISTS替代IN时,只要找到第一条匹配就短路退出,适合“是否存在”类判断;而IN需要确认整个子结果集(除非优化器重写)
相关子查询 vs. 不相关子查询:看是否依赖外层字段
有没有引用外层表的列,直接决定执行次数和可优化空间。这是最容易混淆、也最影响性能的地方。
- 不相关子查询(如
WHERE id IN (SELECT user_id FROM logs WHERE type='login')):只执行一次,结果可缓存 - 相关子查询(如
WHERE salary > (SELECT AVG(salary) FROM emp e2 WHERE e2.dept = emp.dept)):外层每行都重新执行,且无法提前物化 - PostgreSQL 的
LATERAL或 MySQL 8.0+ 的派生表 +JOIN可显式控制相关性,避免隐式循环
EXPLAIN 看不见的陷阱:别只信 “rows” 字段
EXPLAIN 输出里 rows 是估算值,尤其对子查询,它常把相关子查询当成“执行一次”,掩盖真实放大效应。
- MySQL 的
EXPLAIN FORMAT=JSON里找attached_condition和using_join_buffer,能发现是否被转成连接 - PostgreSQL 要看
SubPlan还是InitPlan:前者每行调用,后者只执行一次 - SQL Server 的执行计划 XML 中,注意
Compute Scalar下是否有嵌套的Table Scan,那是相关子查询的典型信号
改写建议:什么情况下该放弃子查询
子查询写起来顺手,但数据库未必买账。当出现慢查询或执行计划异常时,优先考虑结构等价的平铺写法。
- 用
JOIN替代IN/EXISTS时,确保关联字段有索引,否则可能更慢(比如大表驱动小表) -
GROUP BY+ 窗口函数(如AVG() OVER (PARTITION BY dept))能替代多数按组聚合的标量子查询,且只扫描一次 - CTE(
WITH)在 PostgreSQL/SQL Server 中可强制物化,但在 MySQL 8.0 默认是“inline 展开”,不等于缓存,需加/*+ MATERIALIZE */提示(MySQL 8.0.22+)
FORMAT=JSON 或 ANALYZE 就容易误判。

















