一、背景案例
在 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
)
原因:
- newerBatch 数据量大,IN 会物化数百万
record_id到内存 - 主键
(version_number, record_id)可被 EXISTS 子查询精确命中 - EXISTS 短路特性在重叠率高时效果更明显(找到即停)
七、一句话总结
IN 是"先把答案全算出来,再去对照";
EXISTS 是"对照一条,有结果立刻告诉我,不用全算完"。数据量越大,EXISTS 的优势越明显。