相关子查询在主表全表扫描时会线性放大执行次数:若外层表无有效过滤条件,子查询将对每行重复执行,如50万行主表即执行50万次,EXPLAIN显示DEPENDENT SUBQUERY且rows乘积远超结果集,耗时近似线性增长。

相关子查询在主表全表扫描时会线性放大执行次数
只要外层查询没加有效过滤条件,相关子查询就会被反复执行——不是“可能”,而是数据库引擎的强制行为。比如 SELECT id, (SELECT name FROM dept WHERE dept.id = emp.dept_id) FROM emp,如果 emp 表有 50 万行且没走索引,这个子查询就真真切切执行 50 万次。
常见错误现象:EXPLAIN 输出中 select_type 显示 DEPENDENT SUBQUERY,同时 rows 列乘积远超结果集大小(例如外层扫描 48 万行,内层也标称扫描 48 万行);实际耗时随主表行数近似线性增长,而非对数或常量级。
- MySQL 5.7/8.0 默认仍用嵌套循环处理,不自动转 semi-join,哪怕子查询本身只查一个带索引的字段
- PostgreSQL 12+ 对部分
EXISTS可 unnest,但一旦子查询含ORDER BY、LIMIT或引用多个外层列,基本退回 loop join - 子查询里哪怕加了
WHERE dept_id = ? AND status = 'active',若没建(dept_id, status)联合索引,照样全表扫dept
全表扫描 + 相关子查询 = 索引下推彻底失效
外层无过滤时,优化器无法把条件“下推”进子查询执行路径,导致原本能走 Index Nested-Loop Join(NLJ)的场景被迫退化。典型表现是:明明 dept.id 有主键,emp.dept_id 也有索引,但 EXPLAIN 的 Extra 字段却显示 Using where 而非 Using index condition。
这是因为物化后的派生表丢失原始索引结构,InnoDB 引擎层收不到下推条件,只能靠 Server 层做全行回表再过滤。
- 联合索引如
(name, age),若子查询写成WHERE age > 25,实际执行是先取全行再判断,I/O 暴增 - 类型不一致(如
emp.dept_id是BIGINT,dept.id是VARCHAR)会触发隐式转换,直接让索引失效 -
WHERE YEAR(hire_date) = 2024这类函数用法,会让hire_date索引完全不可用
改 JOIN 不等于性能自动提升,驱动表选错反而更慢
把 SELECT * FROM emp WHERE EXISTS (SELECT 1 FROM dept WHERE dept.id = emp.dept_id) 改成 JOIN 是对的,但直接写 emp JOIN dept 可能更糟——优化器可能选 dept 当驱动表,而它只有几十行,emp 却有上百万行,NLJ 效率反而暴跌。
实操关键点在于控制连接顺序和预过滤:
- 优先让小结果集当驱动表:比如先
SELECT id FROM dept WHERE status = 'active'得到 200 行,再用这 200 行去驱动emp - 用
STRAIGHT_JOIN强制顺序(仅当EXPLAIN显示优化器选错时),例如SELECT STRAIGHT_JOIN e.* FROM (SELECT id FROM dept WHERE status = 'active') d JOIN emp e ON e.dept_id = d.id - 避免直接
INNER JOIN导致行数膨胀:一对多关系下,emp一行可能匹配dept多行,结果重复;此时应补DISTINCT或改用EXISTS
真正难的不是语法改写,而是识别哪一层在重复计算
很多 SQL 看似只有一层子查询,但嵌套在视图、CTE 或函数里后,执行计划可能已变成三层依赖。最隐蔽的坑是:外层加了 LIMIT 10,你以为只算 10 行,其实子查询仍按全表执行——因为 MySQL 5.7 及以前版本不支持子查询级的 LIMIT 下推。
验证是否踩坑的唯一可靠方式,是看 EXPLAIN FORMAT=JSON 里的 "rows_examined_per_scan" 和 "materialization" 字段,而不是只盯着最终执行时间。缓存会让时间失真,但扫描行数不会说谎。

















