首页/数据/SQL 速查/子查询与 JOIN 的选择

子查询与 JOIN 的选择

同一需求可以写成 IN/EXISTS 或 JOIN:核心差异在“是否放大行数”与“是否输出另一表列”,性能以执行计划实测为准。

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

子查询与 JOIN 怎么选

同一个需求可以写成 IN / EXISTS,也可以写成 JOIN,实质差异只有两点:是否放大行数、是否需要输出副表的列。只做筛选时用 EXISTS / IN,语义是半连接,一对多关系也不会产生重复行;需要副表字段时才用 JOIN,并接受结果按匹配条数展开。性能上不存在通用结论——优化器会把符合条件的 IN 改写成半连接,最终以执行计划和真实数据分布为准。

基本信息

IN / EXISTS半连接语义,不放大行数,每行至多输出一次
JOIN按匹配行数展开,同时输出两侧列
去重代价JOIN 后要 DISTINCT 或 GROUP BY 才能消除放大,代价不低
优化器行为MySQL 8 半连接或物化;PostgreSQL 常改写为哈希半连接
判断依据EXPLAIN 计划与真实数据分布,而不是「子查询更慢」这类经验之谈

典型使用场景

  • 只判断存在与否:用 EXISTS,短路求值最省
  • 按名单批量筛选:用 IN 子查询,语义清晰
  • 需要展示订单编号、金额等副表字段:用 JOIN
  • 统计每行匹配数量:用 LEFT JOIN + COUNT 或聚合子查询
  • 排查结果重复:先确认是不是一对多连接忘了去重

语义对比

语义对比
-- 只筛选:半连接,不放大行数
SELECT u.id FROM users u
WHERE EXISTS (SELECT 1 FROM orders o
              WHERE o.user_id = u.id AND o.amount > 100);

-- 输出订单列:一对多放大行数,同一用户出现多次
SELECT u.id, o.id AS order_id, o.amount
FROM users u JOIN orders o ON o.user_id = u.id
WHERE o.amount > 100;

需要每行一条时用聚合

需要每行一条时用聚合
SELECT u.id,
       COUNT(o.id)  AS order_cnt,          # COUNT(子表列) 不会把无匹配行算成 1
       SUM(o.amount) AS amount_sum
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;

-- 等价但更依赖逐行执行的写法(行数大时更慢)
SELECT u.id,
       (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_cnt
FROM users u;

按目标选写法

目标推荐写法原因
只判断存在EXISTS短路求值,不放大行数
按名单筛选IN 子查询语义直观,可被改写为半连接
取副表字段JOIN必须输出副表列,放大是预期行为
每行匹配计数LEFT JOIN + COUNT 或聚合子查询需要折叠而不是展开
需要去重的完整结果JOIN + GROUP BY比 DISTINCT 在大结果集上更可控
改写前后都跑一次 EXPLAIN:MySQL 8 会把满足条件的 IN 改写成半连接,PostgreSQL 常用哈希半连接,两者的差异主要体现在估算行数与内存占用。
用 JOIN 代替 EXISTS 再靠 DISTINCT 去重是常见陷阱:DISTINCT 需要对放大后的结果做排序或哈希,代价往往超过筛选本身。

易错点

  • 用 JOIN 代替 IN 却忘记 DISTINCT,结果出现大量重复行
  • 断言「子查询一定更慢」,把高效的半连接改写成更差的 JOIN
  • 用 DISTINCT 消除重复行,掩盖了一对多关系本身没有处理清楚的问题
  • 把适合聚合的场景写成逐行相关子查询,行数与耗时同时失控

数据来源与参考

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