PostgreSQL EXISTS vs IN 性能对比详解

一、背景案例

在 Magellan 数据压缩场景中,需要删除旧批次(olderBatch)中与新批次(newerBatch)重叠的数据。

IN 写法

sql 复制代码
DELETE FROM magellan_cn_order_inbound
WHERE version_number = '20260521172049432'
AND order_id IN (
    SELECT order_id
    FROM magellan_cn_order_inbound b
    WHERE b.version_number = '20260522000707011'
)

EXISTS 写法

sql 复制代码
DELETE FROM magellan_cn_order_inbound
WHERE version_number = '20260521172049432'
AND EXISTS (
    SELECT 1
    FROM magellan_cn_order_inbound b
    WHERE b.version_number = '20260522000707011'
      AND b.order_id = magellan_cn_order_inbound.order_id
)

二、执行逻辑的本质区别

IN 的执行逻辑

复制代码
第一步:执行子查询,物化整个结果集到内存
        SELECT order_id FROM ... WHERE version_number = 'newerBatch'
        → 结果集:[id_1, id_2, id_3, ... id_500万]  全部加载到内存

第二步:对外层每一行,在结果集中做查找
        WHERE order_id IN [id_1, id_2, ... id_500万]

问题:

  • 子查询必须全部执行完才能开始外层过滤
  • 500 万条 record_id 全部物化到内存,内存压力大
  • 如果子查询结果集非常大,可能触发 Hash 构建,占用大量工作内存(work_mem

EXISTS 的执行逻辑

复制代码
第一步:外层取一行(olderBatch 中的某条记录)

第二步:拿这行的 record_id 去子查询中查找
        SELECT 1 FROM ... WHERE version_number = 'newerBatch'
                             AND order_id = '当前行的id'

第三步:找到第一条匹配 → 立即返回 TRUE,短路退出子查询
        没找到 → 返回 FALSE

第四步:重复处理外层下一行

优势:

  • 子查询不需要物化全部结果,找到即停
  • 每次子查询都能命中主键索引 (version_number, record_id),极快
  • 内存占用极低

三、结合本案例的索引分析

复制代码
表主键:(version_number, order_id)
操作 IN EXISTS
子查询索引使用 全扫 newerBatch 所有行,物化 每次用主键精确查 (newerBatch, 当前id)
外层索引使用 version_number=olderBatch 命中 同左
内存使用 高(物化整个子查询) 极低(逐行匹配)
短路优化 ❌ 无 ✅ 找到即停

四、执行计划对比(示意)

IN 的典型执行计划

复制代码
Delete on magellan_cn_order_inbound
  -> Hash Semi Join
       Hash Cond: (a.record_id = b.record_id)
       -> Index Scan on magellan_cn_order_inbound  (version=olderBatch)
       -> Hash
            -> Index Scan on magellan_cn_order_inbound b (version=newerBatch)
                ★ 先把 newerBatch 全部扫出来构建 Hash 表

EXISTS 的典型执行计划

复制代码
Delete on magellan_cn_order_inbound
  -> Nested Loop Semi Join
       -> Index Scan on magellan_cn_order_inbound  (version=olderBatch)
       -> Index Scan on magellan_cn_order_inbound b
            Index Cond: (version_number=newerBatch AND record_id=外层当前id)
            ★ 每次用主键精确查,找到即停

PostgreSQL 实际上很智能,对 IN 也可能优化为 Hash Semi Join,但当子查询结果集超过 work_mem 时,性能会急剧下降。


五、什么时候 IN 反而更快?

EXISTS 也不是万能的,以下场景 IN 可能更优:

场景 推荐
子查询结果集很小(几十条) IN(结果集小,物化代价低,逻辑简单)
子查询结果集很大(百万级) EXISTS(避免物化,逐行匹配更省内存)
外层表数据量远小于子查询 IN(外层循环少,Hash 查找快)
需要去重语义 IN(自动去重)

六、本案例结论

复制代码
magellan_cn_order_inbound 数据量:千万级
newerBatch 数据量:可能 200万~500万条

推荐使用 EXISTS:

sql 复制代码
DELETE FROM magellan_cn_order_inbound
WHERE version_number = #{olderBatch}
AND EXISTS (
    SELECT 1
    FROM magellan_cn_order_inbound b
    WHERE b.version_number = #{newerBatch}
      AND b.order_id = magellan_cn_order_inbound.order_id
)

原因:

  1. newerBatch 数据量大,IN 会物化数百万 record_id 到内存
  2. 主键 (version_number, record_id) 可被 EXISTS 子查询精确命中
  3. EXISTS 短路特性在重叠率高时效果更明显(找到即停)

七、一句话总结

IN 是"先把答案全算出来,再去对照";
EXISTS 是"对照一条,有结果立刻告诉我,不用全算完"。

数据量越大,EXISTS 的优势越明显。

相关推荐
777VG1 天前
PostgreSQL +martin将多张表输出成一个 MVT
前端·数据库·postgresql
IvorySQL1 天前
PG 日报|优化缓冲区批量扫描,降低多套接字并发竞争
数据库·人工智能·postgresql·开源·区块链
muddjsv2 天前
SQLite与MySQL/PostgreSQL本质区别:运行模型、并发、部署、权限全方位对比
mysql·postgresql·sqlite
pxzsky2 天前
PG17数据库安装中分分词插件:pg_jieba
数据库·postgresql·pg_jieba
jun_bai2 天前
postgresql数据库免安装版安装到windows系统
数据库·postgresql
BullSmall2 天前
CentOS7 阿里云 PG 镜像 PG10.9 → PG17 完整升级指南
阿里云·postgresql·centos
瀚高PG实验室2 天前
DELETE 条件字段缺少索引导致 SuperSync 同步严重延迟
数据库·postgresql·瀚高数据库
IvorySQL3 天前
PG 日报|修复高 IO 并发场景,解决预读机制耗尽本地缓冲区缺陷
数据库·人工智能·sql·postgresql·区块链
想你依然心痛3 天前
电力能耗系统怎么搭,Django 加 PostgreSQL 入门指南
postgresql·django·电力能耗系统,后端开发
凌虚4 天前
基于 PostgreSQL WAL 构建 CDC 系统:原理与工程实现
数据库·后端·postgresql