dependent subquery会被反复执行,因其本质是相关子查询,需对外层每行执行一次,10万行外层即触发10万次内层执行;只要子查询引用外层列(如b.a_id = a.id),优化器就放弃预计算,导致mysql/postgresql均退化为嵌套循环,即使走索引也难逃线性放大效应。

Dependent Subquery 为什么会被反复执行
PostgreSQL 和 MySQL 都会把 Dependent Subquery(相关子查询)当作“对外层每行都重算一次”的逻辑来处理。比如外层表有 10 万行,子查询哪怕只查一个字段、走索引,也会被执行 10 万次 —— 这不是缓存失效的问题,是执行引擎的固有行为。
常见错误现象:EXPLAIN 输出中看到 Dependent subquery 或 select_type=DEPENDENT SUBQUERY,且 rows 列乘积远超预期;实际执行时间随外层行数线性增长。
- MySQL 5.7+ 默认仍用嵌套循环处理这类子查询,即使子查询本身很快,放大效应也极明显
- PostgreSQL 在 12+ 版本虽支持部分 unnest 优化,但遇到
WHERE ... IN (SELECT ...)或标量子查询仍大概率退化为 loop join - 如果子查询里用了
ORDER BY ... LIMIT 1,还可能触发额外排序开销,进一步拖慢
哪些写法容易触发 Dependent Subquery
不是所有子查询都会被判定为相关子查询,关键看是否引用了外层列。只要子查询的 WHERE、ON、HAVING 或标量表达式中出现外层表别名,优化器基本就放弃预计算。
典型场景:
-
SELECT a.id, (SELECT b.name FROM b WHERE b.a_id = a.id LIMIT 1) FROM a→ 标量子查询,必相关 -
SELECT * FROM a WHERE a.id IN (SELECT b.a_id FROM b WHERE b.status = 'active' AND b.a_id = a.id)→b.a_id = a.id让子查询绑定外层 -
SELECT * FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.a_id = a.id AND b.created_at > a.updated_at)→ 引用了两个外层列,更难优化
注意:IN 子句若右侧无外层引用(如 WHERE id IN (SELECT id FROM tmp_ids)),就不会被标记为 dependent,此时可走 hash semi-join。
替换成 JOIN 时要注意字段去重和 NULL 行
直接把相关子查询改写成 LEFT JOIN 是最常用解法,但容易忽略语义差异:子查询天然“至多返回一行”,而 JOIN 可能因一对多关系产生重复行或漏行。
- 标量子查询改
LEFT JOIN后,必须加GROUP BY或用窗口函数取首行,否则结果错乱 - 用
ROW_NUMBER() OVER (PARTITION BY a.id ORDER BY b.updated_at DESC)替代LIMIT 1更可控 -
EXISTS改LEFT JOIN ... ON ... WHERE b.id IS NOT NULL时,要确认b.id非空,否则 NULL 值会导致整行被过滤掉 - 如果原查询依赖子查询返回
NULL表示“不存在”,改 JOIN 后需显式补COALESCE(..., NULL)逻辑
什么时候不该硬转 JOIN?考虑物化或临时表
当子查询本身很重(如含聚合、多表关联、全表扫描),且外层数据量不大(
- MySQL 可用
CREATE TEMPORARY TABLE tmp_b AS SELECT ...预计算,再与外层JOIN - PostgreSQL 用
WITH b_pre AS (SELECT ...) SELECT ... FROM a LEFT JOIN b_pre ON ...,配合MATERIALIZED提示(v12+)强制物化 - 避免在子查询里写
SELECT *,只选真正需要的字段,减少临时结果集大小 - 若子查询条件固定(如只按某状态筛选),可在其结果上建内存索引(PG 的
CREATE INDEX ON tmp_b(a_id))加速关联
真正棘手的是那种“外层大、子查询也重、还带排序分页”的组合——这时候没有银弹,得拆成两步:先取外层 ID 列表,再用 IN 批量查子查询结果,中间加缓存或异步预热更现实。










