子查询改连接值不值
判断「另一张表里有没有对应数据」有两种天然写法:写成存在性判断的子查询,或者改成连接后再去掉重复。现代优化器大多能把存在性判断转换成半连接,两者执行代价接近,因此选择依据应该回到语义本身——子查询版本天然不产生重复行,连接版本需要额外的去重动作,去重本身可能比省下来的扫描更贵。真正影响性能的往往不是写法,而是被查的列上有没有索引。
基本信息
| 适用平台 | 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);写法对照
| 写法 | 行数特性 | 适用情况 |
|---|---|---|
| 存在性判断 | 每行至多输出一次 | 只要判断有无匹配 |
| 连接加去重 | 连接阶段会被放大,去重后收敛 | 需要匹配表的若干列时 |
| 连接加分组 | 按分组键收敛 | 需要计数或聚合 |
| 集合判断取反 | 遇到空值结果可能为空 | 建议改成存在性判断的否定形式 |
| 标量子查询 | 要求至多一行 | 只取一个值 |
取反集合判断遇到空值时的行为是经典陷阱:当被比较的列里存在空值时,整个条件不会成立,查询结果为空却不报错。改用存在性判断的否定形式可以彻底避开这个问题。
不要为了「优化」一律把存在性判断改成连接:连接会按匹配行数放大结果,被迫加上去重或分组,数据分布不利时反而更慢。改写前后都要用计划与真实耗时验证。