子查询与 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 需要对放大后的结果做排序或哈希,代价往往超过筛选本身。