最常用方式是LEFT JOIN + IS NULL,需用唯一键JOIN且WHERE中用IS NULL而非= NULL;NOT EXISTS更安全,避免重复和NULL陷阱;双向差异可用EXCEPT但需字段一致并注意NULL处理;WHERE条件位置关键,过滤表A应写WHERE,限制表B匹配范围才写ON。

用 LEFT JOIN + IS NULL 找出表A有但表B没有的记录
这是最常用也最直观的方式,核心思路是把表A作为主表,LEFT JOIN 表B,再筛选表B中匹配字段为 NULL 的行。注意:必须用被比较字段(如主键或唯一业务键)JOIN,且该字段在表B中允许为 NULL(JOIN 后才可能为 NULL)。
常见错误是写成 WHERE b.id = NULL —— 这永远不成立,得用 IS NULL;另一个坑是没加 ON 条件或条件写错(比如漏掉多字段匹配中的某一个),导致笛卡尔积式膨胀。
示例(找 orders 里存在但 order_logs 里缺失对应记录的订单):
SELECT o.* FROM orders o LEFT JOIN order_logs l ON o.order_id = l.order_id WHERE l.order_id IS NULL;
用 NOT EXISTS 替代 LEFT JOIN,避免重复和 NULL 语义陷阱
当表B的匹配字段存在重复值,或你只关心“是否存在”而非“关联多少条”,NOT EXISTS 更安全。它不会因为表B的重复导致主表记录被放大,也不依赖 IS NULL 的理解门槛。
性能上,多数数据库对 NOT EXISTS 有较好优化(尤其带索引时),但某些旧版本 MySQL 在子查询无索引时会变慢。务必确认子查询里的关联字段已建索引。
示例(语义同上,但更健壮):
SELECT * FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM order_logs l WHERE l.order_id = o.order_id );
双向差异:UNION ALL + EXCEPT(或 MINUS)要小心字段顺序和 NULL 处理
如果既要找 A 有 B 没有,又要找 B 有 A 没有,直接拼两个 NOT EXISTS 或两个 LEFT JOIN 更清晰。想用集合运算(如 EXCEPT)时,必须保证两查字段数量、类型、顺序完全一致,否则报错或结果错乱。
EXCEPT 默认去重,而 EXCEPT ALL(PostgreSQL 支持)保留重复;SQL Server 用 EXCEPT,Oracle 用 MINUS;MySQL 8.0+ 才支持 EXCEPT,之前只能靠 LEFT JOIN 模拟。
关键细节:NULL 值在 EXCEPT 中被视为相等,但如果你的业务字段允许 NULL,且想区分“真缺失”和“NULL 值”,就得提前用 COALESCE(col, '>') 统一处理。
WHERE 条件写在哪?JOIN 条件 vs 过滤条件容易混淆
这是实际写错最多的地方。如果把本该写在 WHERE 的过滤(比如只比对状态为 'active' 的订单)错写进 ON,会导致 LEFT JOIN 失效——表B的行被提前筛掉,IS NULL 判定失准。
口诀:JOIN 条件只管“怎么连”,WHERE 条件才管“连完后留哪些”。需要过滤表A,写在 WHERE;需要过滤表B参与匹配的范围(比如只用最近30天的日志比对),才写在 ON 里。
错误示范(status = 'active' 写在 ON,导致非 active 的订单也被当作“B 缺失”):
LEFT JOIN order_logs l ON o.order_id = l.order_id AND l.status = 'active' -- ❌
正确写法:
LEFT JOIN order_logs l ON o.order_id = l.order_id WHERE o.status = 'active' AND l.order_id IS NULL; -- ✅
真正麻烦的不是语法,而是搞清你要比的是“全量数据差异”,还是“满足某条件下的差异”——前者字段对齐就行,后者稍一疏忽,ON 和 WHERE 位置颠倒,结果就全偏了。











