为什么SQL非相关子查询执行计划只缓存一次,其执行结果如何被重用?
- 内容介绍
- 相关推荐
本文共计969个文字,预计阅读时间需要4分钟。
非相关子查询仅执行一次,并非语法规定,而是优化器主动进行的省事动作——它识别出子查询不依赖于外部数据,便将其结果一次性计算出,存储在内存中的常量区域或临时物化表中,后续重复使用。
怎么快速判断一个子查询是不是非相关的
核心就一条:子查询里有没有出现外层表的列名(比如 t1.id、orders.user_id)。
- 没有——就是非相关,例如
(SELECT MAX(price) FROM products)、(SELECT value FROM config WHERE key = 'timeout') - 有——就是相关,例如
(SELECT COUNT(*) FROM logs WHERE user_id = users.id),哪怕只多写了一个点号,也会触发逐行重算 - 验证方法:把子查询整段复制出来,单独
SELECT一把,能跑通且不报“Unknown column”就是非相关
EXPLAIN 里怎么看它到底执行了几次
不能只看 select_type 是 SUBQUERY 还是 DEPENDENT SUBQUERY,得结合执行计划结构来确认实际行为。
- MySQL 8.0+:用
EXPLAIN FORMAT=TREE,如果看到子查询被包在-> Materialize下,说明已被物化,只执行一次 - PostgreSQL:用
EXPLAIN (ANALYZE, VERBOSE),若子查询显示SubPlan且actual time=0.001..0.002 rows=1,且never executed没出现,基本就是单次计算 - 注意陷阱:
DEPENDENT SUBQUERY未必真“每行都跑”——某些版本会做 semi-join 优化,但只要子查询里写了外层列,就不能默认它被优化掉了
哪些情况会让“本该只执行一次”的子查询变慢
非相关 ≠ 绝对安全。以下几种写法会让优化器放弃物化,或者让单次执行本身就很重。
- 子查询含
LIMIT但没ORDER BY:MySQL 可能拒绝物化,因为结果不稳定 - 子查询引用了函数如
NOW()、RAND()或用户变量@var:优化器认为结果不可缓存,每次都会重求值 - 子查询返回多行多列却用在标量上下文(如
WHERE x = (SELECT a,b FROM t)):会报错,但某些旧版 MySQL 只取第一行,还可能漏掉优化路径 - SQLite 旧版本(
为什么改写成 JOIN 有时反而更慢
物化是优化,JOIN 是重写逻辑——两者目标不同,不能简单互换。
- 非相关子查询本质是“查一个常量”,比如
WHERE status = (SELECT id FROM status_codes WHERE code = 'active');改成JOIN status_codes ON ...强制关联,可能引入多余行或 NULL 匹配,语义已变 - 如果子查询带聚合(如
AVG(salary)),JOIN 后再GROUP BY容易放大中间结果集,尤其是外层表大、聚合字段无索引时 - 真正该优先 JOIN 的,是那些本就该“一对多展开”的场景,比如“查每个部门的平均薪资”,而不是“查所有人的薪资是否高于全局平均”
最容易被忽略的一点:非相关子查询的“只执行一次”,完全依赖优化器能否静态判定其独立性。任何对外部上下文的隐式依赖(比如 session 变量、临时表、函数副作用),都会让这个假设失效——这时候看执行计划比背概念管用得多。
本文共计969个文字,预计阅读时间需要4分钟。
非相关子查询仅执行一次,并非语法规定,而是优化器主动进行的省事动作——它识别出子查询不依赖于外部数据,便将其结果一次性计算出,存储在内存中的常量区域或临时物化表中,后续重复使用。
怎么快速判断一个子查询是不是非相关的
核心就一条:子查询里有没有出现外层表的列名(比如 t1.id、orders.user_id)。
- 没有——就是非相关,例如
(SELECT MAX(price) FROM products)、(SELECT value FROM config WHERE key = 'timeout') - 有——就是相关,例如
(SELECT COUNT(*) FROM logs WHERE user_id = users.id),哪怕只多写了一个点号,也会触发逐行重算 - 验证方法:把子查询整段复制出来,单独
SELECT一把,能跑通且不报“Unknown column”就是非相关
EXPLAIN 里怎么看它到底执行了几次
不能只看 select_type 是 SUBQUERY 还是 DEPENDENT SUBQUERY,得结合执行计划结构来确认实际行为。
- MySQL 8.0+:用
EXPLAIN FORMAT=TREE,如果看到子查询被包在-> Materialize下,说明已被物化,只执行一次 - PostgreSQL:用
EXPLAIN (ANALYZE, VERBOSE),若子查询显示SubPlan且actual time=0.001..0.002 rows=1,且never executed没出现,基本就是单次计算 - 注意陷阱:
DEPENDENT SUBQUERY未必真“每行都跑”——某些版本会做 semi-join 优化,但只要子查询里写了外层列,就不能默认它被优化掉了
哪些情况会让“本该只执行一次”的子查询变慢
非相关 ≠ 绝对安全。以下几种写法会让优化器放弃物化,或者让单次执行本身就很重。
- 子查询含
LIMIT但没ORDER BY:MySQL 可能拒绝物化,因为结果不稳定 - 子查询引用了函数如
NOW()、RAND()或用户变量@var:优化器认为结果不可缓存,每次都会重求值 - 子查询返回多行多列却用在标量上下文(如
WHERE x = (SELECT a,b FROM t)):会报错,但某些旧版 MySQL 只取第一行,还可能漏掉优化路径 - SQLite 旧版本(
为什么改写成 JOIN 有时反而更慢
物化是优化,JOIN 是重写逻辑——两者目标不同,不能简单互换。
- 非相关子查询本质是“查一个常量”,比如
WHERE status = (SELECT id FROM status_codes WHERE code = 'active');改成JOIN status_codes ON ...强制关联,可能引入多余行或 NULL 匹配,语义已变 - 如果子查询带聚合(如
AVG(salary)),JOIN 后再GROUP BY容易放大中间结果集,尤其是外层表大、聚合字段无索引时 - 真正该优先 JOIN 的,是那些本就该“一对多展开”的场景,比如“查每个部门的平均薪资”,而不是“查所有人的薪资是否高于全局平均”
最容易被忽略的一点:非相关子查询的“只执行一次”,完全依赖优化器能否静态判定其独立性。任何对外部上下文的隐式依赖(比如 session 变量、临时表、函数副作用),都会让这个假设失效——这时候看执行计划比背概念管用得多。

