运营说"帮我找出买了 A 类商品但没买 B 类的用户",你第一反应是什么?LEFT JOIN + IS NULL?NOT EXISTS?其实 SQL 早就给了你一行解法。
你可能从未用过它们
大多数分析师写了三年 SQL,都没碰过 EXCEPT 和 INTERSECT。
不是它们没用——而是很多人根本不知道有这两个关键字。
日常工作中,"找差异"和"找交集"的需求非常高频:
传统写法?LEFT JOIN + IS NULL,或者 NOT EXISTS 嵌套子查询。能跑,但可读性很差
花 5 分钟,把这两个运算函数彻底拿下。
先用一句话讲清楚
INTERSECT = 两边都有的(交集)
EXCEPT = 左边有、右边没有的(差集)
记住这张图:
INTERSECT: 🟦 ∩ 🟩 → 重叠的部分
EXCEPT: 🟦 - 🟩 → 🟦 独有的部分
INTERSECT:找出"两边都有的人"
基础语法
SELECT user_id FROM orders_202604
INTERSECT
SELECT user_id FROM orders_202605;
结果:同时在 4 月和 5 月都下过单的用户。
一个真实的业务场景
运营想办一场"老用户回馈"活动,条件是——
"3 月份买过,而且 5 月份也买过的用户"
SELECT user_id
FROM orders
WHERE order_date BETWEEN'2026-03-01'AND'2026-03-31'
AND status ='已完成'
INTERSECT
SELECT user_id
FROM orders
WHERE order_date BETWEEN'2026-05-01'AND'2026-05-31'
AND status ='已完成';
干净、直白、没有 JOIN 的纠缠。
任何人看一眼就知道这段 SQL 在干什么。
和传统写法的对比
同样的需求,用 JOIN 写:
SELECT a.user_id
FROM (
SELECTDISTINCT user_id FROM orders
WHERE order_date BETWEEN'2026-03-01'AND'2026-03-31'
AND status ='已完成'
) a
JOIN (
SELECTDISTINCT user_id FROM orders
WHERE order_date BETWEEN'2026-05-01'AND'2026-05-31'
AND status ='已完成'
) b ON a.user_id = b.user_id;
功能一样,但多了三层嵌套、一个 JOIN、一个别名。
哪个更好维护,一目了然。
EXCEPT:找出"有 A 没有 B 的人"
基础语法
SELECT user_id FROM orders_202604
EXCEPT
SELECT user_id FROM orders_202605;
结果:4 月买过但 5 月没买过的用户——这就是你的流失预警名单。
核心应用:用户流失分析
"帮我拉一下上个月有消费、这个月没有的用户。"
一句话需求,EXCEPT 一行搞定:
SELECT user_id
FROM orders
WHERE order_date BETWEEN'2026-04-01'AND'2026-04-30'
AND status ='已完成'
EXCEPT
SELECT user_id
FROM orders
WHERE order_date BETWEEN'2026-05-01'AND'2026-05-31';
用 NOT IN 能做到吗?
能,但有一个致命陷阱。
-- 看似等价,其实有坑
SELECT user_id
FROM orders
WHERE order_date BETWEEN'2026-04-01'AND'2026-04-30'
AND status ='已完成'
AND user_id NOTIN (
SELECT user_id FROM orders
WHERE order_date BETWEEN'2026-05-01'AND'2026-05-31'
);
如果子查询的结果里包含 NULL,NOT IN 会返回空结果。 整个查询白跑。
而 EXCEPT 自动处理 NULL,天然安全。
这是一个很多人踩过的坑,值得单独强调。
三个实战场景
场景一:A/B 测试人群隔离检查
上线 A/B 测试前,运营问:"实验组和对照组有没有重叠用户?"
SELECT user_id FROM ab_test_group WHERE group_name = '实验组'
INTERSECT
SELECT user_id FROM ab_test_group WHERE group_name = '对照组';
如果有结果返回——赶紧找人修数据,实验结论不可信。
场景二:多渠道用户去重
市场部投了抖音和微信两个渠道,需要看各渠道的独占用户。
-- 只在抖音注册、没有在微信注册的用户
SELECT user_id FROM channel_users WHERE channel = '抖音'
EXCEPT
SELECT user_id FROM channel_users WHERE channel = '微信';
反过来换一下条件,就能拿到微信独占用户。两个渠道的增量价值一目了然。
场景三:数据一致性校对
财务系统和订单系统的数据对不上?快速找差异:
SELECT order_id FROM finance_orders
EXCEPT
SELECT order_id FROM biz_orders;
有结果就说明存在单边账——财务有记录但业务没有,或者反过来。排查范围瞬间缩小。
四条铁律,避免翻车
铁律一:列数和顺序必须一致
-- 正确 ✅ 两边都是 1 列
SELECT user_id FROM table_a
EXCEPT
SELECT user_id FROM table_b;
-- 报错 ❌ 左边 2 列,右边 1 列
SELECT user_id, order_id FROM table_a
EXCEPT
SELECT user_id FROM table_b;
EXCEPT 和 INTERSECT 不像 JOIN 靠列名匹配。 它们是按位置一一比较的,列数不同直接报错。
铁律二:默认去重
-- 如果左边有 3 条 user_id = 'U001',右边也有 2 条
-- EXCEPT 结果中 'U001' 只出现 1 次
SELECT user_id FROM orders_202604
EXCEPT
SELECT user_id FROM orders_202605;
大多数场景下这是你想要的行为。如果确实需要保留重复行,看下一条。
铁律三:不去重用 ALL
-- EXCEPT ALL:保留重复
SELECT user_id FROM orders_202604
EXCEPT ALL
SELECT user_id FROM orders_202605;
带 ALL 时,逻辑变成**"左边比右边多出来的部分"**。比如左边 3 个 U001,右边 2 个,结果会保留 1 个。
实际工作中 90% 的情况用不带 ALL 的版本就够了。知道有这个选项即可。
数据库兼容性
一张速查表,收藏备用
┌──────────────────────────────────────────────────┐
│ INTERSECT 两边都有的(交集) │
│ EXCEPT 左边有、右边没有的(差集) │
│ ... ALL 保留重复行(不去重) │
│ │
│ ⚠️ 列数必须相同,按位置比较 │
│ ⚠️ 默认去重(DISTINCT 行为) │
│ ⚠️ MySQL 8.0+ 才支持 │
│ ⚠️ 比 NOT IN 更安全(自动处理 NULL) │
└──────────────────────────────────────────────────┘
最后说两句
EXCEPT 和 INTERSECT 不是什么高深语法。它们只是太简单了,简单到很多人忽略了。
但好的 SQL 不是写得复杂——是写得简单,让下一个看代码的人 3 秒钟就能理解你在干什么,那些好用但是少见的SQL函数。
该文章在 2026/5/27 18:25:28 编辑过