Semi-Join(半连接)通俗讲解
本文用通俗的方式解释数据库里的 Semi-Join(半连接) ,并说明为什么
IN (子查询)里通常 不需要手动加DISTINCT。文中所有表名、字段名均为脱敏示例,与实际项目无关。
1. 一句话理解
Semi-Join(半连接) :判断「左表的每一行,在右表里是否至少存在一条匹配」。
- 存在 → 保留这一行
- 不存在 → 丢弃这一行
关键点:只关心「有没有」,不关心「有几条」。左表每行最多输出一次,右表匹配到几条都不会让左表这行变多。
WHERE ... IN (子查询) 和 WHERE EXISTS (子查询) 都会被数据库优化成 Semi-Join。
2. 用生活例子理解
假设有两张表(脱敏示例):
student(学生表)
| student_id | name |
|---|---|
| 1 | 小明 |
| 2 | 小红 |
| 3 | 小刚 |
exam_record(考试记录表)
| record_id | student_id | subject |
|---|---|---|
| 101 | 1 | 数学 |
| 102 | 1 | 英语 |
| 103 | 1 | 物理 |
| 104 | 2 | 数学 |
现在问题是:「哪些学生参加过考试?」
sql
SELECT *
FROM student
WHERE student_id IN (SELECT student_id FROM exam_record);
结果:
| student_id | name |
|---|---|
| 1 | 小明 |
| 2 | 小红 |
- 小明(id=1)在考试记录里出现了 3 次 ,但结果里只出现 1 次。
- 小刚(id=3)没有任何考试记录,被丢弃。
这就是 Semi-Join:只判断「有没有」,重复不会放大结果。
3. 对比:普通 JOIN 会放大
如果用普通 JOIN,结果会不一样:
sql
SELECT s.*
FROM student s
JOIN exam_record e ON s.student_id = e.student_id;
结果:
| student_id | name |
|---|---|
| 1 | 小明 |
| 1 | 小明 |
| 1 | 小明 |
| 2 | 小红 |
小明出现了 3 次!因为普通 JOIN 是「一对多」全部展开。
| 对比项 | Semi-Join(IN/EXISTS) |
普通 JOIN |
|---|---|---|
| 关心匹配数量 | 否,只看有没有 | 是,全部展开 |
| 左表行会被放大吗 | 不会 | 会 |
| 右表重复的影响 | 无影响 | 会导致重复行 |
4. 回到核心问题:IN 子查询要不要加 DISTINCT?
看这条更新语句(脱敏示例,把「有子记录的父订单」打个标记):
sql
UPDATE orders
SET parent_flag = 'Y'
WHERE parent_flag IS DISTINCT FROM 'Y'
AND order_id IN (
SELECT parent_order_id -- 这里会有大量重复值
FROM orders
WHERE version = 'v1'
AND child_flag = 'Y'
AND parent_order_id IS NOT NULL
);
子查询里的 parent_order_id 确实会大量重复(很多子订单指向同一个父订单)。
那要不要写成 SELECT DISTINCT parent_order_id?答案:不需要。
为什么不需要?
-
语义上不会出错
IN是 Semi-Join,order_id只要在子查询里「存在」就更新,存在 1 次和存在 100 次效果完全一样。外层order_id是唯一的,最多被更新一次。重复值不会造成重复更新。 -
加 DISTINCT 反而更慢
- 加了
DISTINCT,数据库要额外做一步去重(排序或哈希聚合),必须先把子查询结果全部算出来再去重。 - 不加
DISTINCT,Semi-Join 可以「边扫描边匹配、找到一条就停」,通常更快。
- 加了
一句话总结
IN/EXISTS子查询里的重复值,数据库会自动帮你「当作存在性判断」处理,手动DISTINCT是多余的,还可能拖慢速度。
5. 什么时候才真的需要去重?
只有当你用的是 普通 JOIN,而且是「一对多」关系时,才需要担心重复放大。例如:
sql
-- 这种 JOIN 会因为一对多而放大行数
SELECT a.record_id, b.material
FROM child_table a
JOIN parent_table b ON b.record_id = a.parent_id;
如果一个父记录对应多个子记录,这里就会产生重复行,可能需要 DISTINCT 或改写。
但 IN (子查询) 不属于这种情况,不用担心。
6. 快速记忆卡
| 场景 | 要不要 DISTINCT | 原因 |
|---|---|---|
WHERE x IN (SELECT y ...) |
❌ 不要 | Semi-Join 自动处理存在性,重复无影响 |
WHERE EXISTS (...) |
❌ 不要 | 同上,找到一条就短路 |
JOIN(一对多) |
✅ 可能要 | 普通 JOIN 会放大行数 |
7. 小结
- Semi-Join = 存在性判断:左表每行只看「右表有没有匹配」,不看有几条。
IN和EXISTS都是 Semi-Join,天然去重,重复子查询值不会放大结果、不会重复更新。- 手动加
DISTINCT在IN子查询里通常是负优化。 - 只有普通
JOIN的一对多场景才需要考虑去重。