NOT EXISTS最可靠,因其语义明确表达“左表某行在右表中找不到匹配”,不依赖NULL处理逻辑,避免NOT IN的UNKNOWN陷阱和LEFT JOIN因多对一导致的漏判。

用 NOT EXISTS 找出左表有但右表没有的记录
最可靠的方式不是用 LEFT JOIN 配合 IS NULL,而是用 NOT EXISTS。它明确表达“左表某行在右表中找不到匹配”,语义清晰,且不会因右表多对一关联产生重复或漏判。
常见错误是写成 SELECT * FROM a WHERE id NOT IN (SELECT id FROM b)——一旦 b.id 中有 NULL,整条 NOT IN 就返回空结果,这是 SQL 三值逻辑的坑。
- 必须确保子查询里的关联条件写全,比如
WHERE b.key = a.key -
NOT EXISTS在大多数数据库(PostgreSQL、SQL Server、Oracle)中能走索引,性能通常优于NOT IN - MySQL 5.7+ 对
NOT EXISTS优化较好,但若子查询里有复杂计算或函数,仍可能退化为嵌套循环
用 LEFT JOIN + IS NULL 补全右侧缺失场景
当你要查出左表所有字段,并同时看到右表“缺哪一列”时,LEFT JOIN 更直观。但它只适用于“一对一或一对零”的业务逻辑;如果右表一条左表主键对应多条记录,IS NULL 判定会失效——因为只要有一条匹配,就不是 NULL。
示例:查所有用户但排除已下单的用户,不能直接 LEFT JOIN orders ON u.id = o.user_id WHERE o.id IS NULL,除非你确认每个用户最多一个订单;否则得先去重或用 NOT EXISTS。
- 务必在
ON子句中写清楚连接条件,别挪到WHERE里,否则会变成内连接 - 如果右表有多个关联字段(如
(user_id, status)),ON条件也要完整覆盖 - 某些旧版 SQLite 或 MySQL 低版本对
LEFT JOIN ... IS NULL的执行计划不友好,建议加复合索引:CREATE INDEX idx_b_key ON b(key)
跨表字段类型或空值导致“看似不匹配”的陷阱
两张表明明数据一样,子查询却返回空?大概率是字段类型不一致或隐式转换惹的祸。比如左表 id 是 VARCHAR 带空格,右表是 INT;或者左表用 '123',右表存的是 123,数据库可能转成字符串比对但忽略末尾空格,也可能严格按类型拒绝匹配。
- 用
CAST(a.id AS CHAR)和CAST(b.id AS CHAR)统一类型再比对 - 检查是否含不可见字符:
LENGTH(a.id) != LENGTH(TRIM(a.id)) -
NULL永远不等于任何值(包括另一个NULL),所以a.id = b.id在任一端为NULL时恒为FALSE,这直接影响NOT EXISTS和JOIN结果
需要双向不匹配(A有B无,且B有A无)怎么办
这不是单个子查询能解决的,得用 UNION ALL 拼两个方向的结果。注意别用 UNION——去重开销大,且如果某行在 A 和 B 中都不存在(不可能),反而干扰逻辑。
典型写法:
SELECT 'in_a_only' AS source, a.* FROM a WHERE NOT EXISTS (SELECT 1 FROM b WHERE b.key = a.key) UNION ALL SELECT 'in_b_only' AS source, b.* FROM b WHERE NOT EXISTS (SELECT 1 FROM a WHERE a.key = b.key)
- 两个子查询的字段数和类型必须严格一致,否则报错
- 如果要查具体哪几列不同(比如同 key 但 name 不同),就得改用
FULL OUTER JOIN——但 MySQL 不支持,得用LEFT JOIN+RIGHT JOIN模拟 - 大数据量下,两次
NOT EXISTS可能比一次哈希差集慢,这时考虑导出 ID 集合用程序比对更稳
实际写的时候,先确认“不匹配”的业务定义:是主键缺失?还是关键字段组合不一致?再选语法。类型对齐和 NULL 处理,往往比写对子查询结构还关键。

















