首页/数据/SQL 速查/子查询改写为连接的取舍
性能与执行计划72 条

子查询改写为连接的取舍

IN/EXISTS 子查询与 JOIN 在多数优化器下可得到等价执行:语义差异(去重、NULL)决定改写方向。

SQL 速查数据更新于 2026-10-13

子查询改连接值不值

判断「另一张表里有没有对应数据」有两种天然写法:写成存在性判断的子查询,或者改成连接后再去掉重复。现代优化器大多能把存在性判断转换成半连接,两者执行代价接近,因此选择依据应该回到语义本身——子查询版本天然不产生重复行,连接版本需要额外的去重动作,去重本身可能比省下来的扫描更贵。真正影响性能的往往不是写法,而是被查的列上有没有索引。

基本信息

适用平台MySQL 8.0 / PostgreSQL 12 及以上
存在性判断只判断有无匹配,不产生重复行,语义上更贴近需求
集合判断与存在性判断常可互换,但遇到空值需要格外小心
连接改写需要配上去重,否则行数被放大
优化器行为MySQL 有半连接优化策略,PostgreSQL 会生成半连接类计划

典型使用场景

  • 筛选有订单的用户:存在性判断或连接去重都可以,看可读性取舍
  • 统计每个用户的订单数:必须用左连接加分组,不能靠存在性判断
  • 排除法筛选:取没有匹配记录的那部分数据
  • 排查取反条件的空值陷阱:结果为空但数据明明存在
  • 慢查询改写:把行数被放大的连接改回存在性判断

三种写法的取舍

三种写法的取舍
-- 存在性判断:语义清晰,不会产生重复行
SELECT u.id, u.name
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

-- 连接加去重:结果等价,但多了一步去重
SELECT DISTINCT u.id, u.name
FROM users u
JOIN orders o ON o.user_id = u.id;

-- 需要计数或聚合时必须用左连接,否则没有订单的用户会消失
SELECT u.id, COUNT(o.id) AS order_cnt
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;

-- 取反条件:用存在性判断的否定形式,避开空值陷阱
SELECT u.id, u.name
FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

写法对照

写法行数特性适用情况
存在性判断每行至多输出一次只要判断有无匹配
连接加去重连接阶段会被放大,去重后收敛需要匹配表的若干列时
连接加分组按分组键收敛需要计数或聚合
集合判断取反遇到空值结果可能为空建议改成存在性判断的否定形式
标量子查询要求至多一行只取一个值
取反集合判断遇到空值时的行为是经典陷阱:当被比较的列里存在空值时,整个条件不会成立,查询结果为空却不报错。改用存在性判断的否定形式可以彻底避开这个问题。
不要为了「优化」一律把存在性判断改成连接:连接会按匹配行数放大结果,被迫加上去重或分组,数据分布不利时反而更慢。改写前后都要用计划与真实耗时验证。

易错点

  • 为了优化一律把存在性判断改成连接,行数被放大后不得不加去重,整体反而更慢
  • 保留取反集合判断不改写:列里存在空值时结果为空,语义与性能双输
  • 把内连接当成存在性判断使用,有多条匹配记录时同一用户重复出现,统计口径被放大
  • 需要一个匹配表的列时仍用存在性判断,被迫在子查询里塞聚合,逻辑绕且难维护
  • 改写后只看估算不看真实耗时,估算偏差大时结论与事实相反

数据来源与参考

  • MySQL 8.0 Reference Manual · Optimizing Subqueries
  • PostgreSQL Documentation · Subquery Expressions
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌