PostgreSQL Semi-Join(半连接)通俗讲解

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?答案:不需要。

为什么不需要?

  1. 语义上不会出错

    IN 是 Semi-Join,order_id 只要在子查询里「存在」就更新,存在 1 次和存在 100 次效果完全一样。外层 order_id 是唯一的,最多被更新一次。重复值不会造成重复更新。

  2. 加 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 = 存在性判断:左表每行只看「右表有没有匹配」,不看有几条。
  • INEXISTS 都是 Semi-Join,天然去重,重复子查询值不会放大结果、不会重复更新。
  • 手动加 DISTINCTIN 子查询里通常是负优化
  • 只有普通 JOIN 的一对多场景才需要考虑去重。
相关推荐
devpotato1 小时前
缓存与数据库更新顺序不一致问题
java·数据库·redis
天若有情6731 小时前
Node+MySQL小型全栈笔记项目实战课程分享
数据库·笔记·mysql
wxwx_bscxy3222 小时前
基于springboot宠物领养系统的设计与实现
数据库·spring boot·后端·spring·宠物
九月要晚安2 小时前
达梦DW与AFC对比
数据库
实战派K8S&DB2 小时前
如何在内网配置 TiDB 数据库 Agent
数据库·人工智能·分布式·tidb
实战派K8S&DB2 小时前
《基于 Dify + FastAPI + PyTiDB 搭建大模型驱动的 TiDB 智能运维 Agent》
运维·数据库·分布式·云原生·tidb·fastapi
SelectDB2 小时前
Apache Doris 5.0 年度版本前瞻(一):构建统一的多模湖仓实时分析平台
大数据·数据库·数据分析
小白学大数据3 小时前
超简单:用 Python 让 Excel 飞起来:用 openpyxl 把重复报表整理交给脚本
开发语言·数据库·python·excel
西索斯coding3 小时前
doubao-seed-2.1-turbo 调用一直 401 怎么办?pro 版同样的 Key 却正常——5 分钟排查定位指南
java·服务器·数据库·ai
袋鼠云数栈3 小时前
整库迁移与分库分表同步:批量数据搬迁的配置简化思路
jvm·数据库·oracle