not exists是最稳妥方式,因其不受null干扰、语义清晰、执行计划可控;需确保子查询关联字段有索引、写select 1、明确关联条件,避免隐式转换和全表扫描。

用 NOT EXISTS 找出表A有但表B没有的记录
这是最常用也最稳妥的方式,尤其当表B有复合主键或需要按多字段匹配时。NOT EXISTS 不受 NULL 值干扰,语义清晰,执行计划通常比 LEFT JOIN + IS NULL 更可控。
常见错误是写成 WHERE col IN (SELECT col FROM B) ——只要 B.col 里有一个 NULL,整个条件就返回空结果,导致漏查。
实操建议:
- 把被检查表(A)放在外层,关联字段必须明确写出,避免隐式类型转换(比如
id是INT,而子查询里用了字符串'1') - 子查询中
SELECT 1就够了,不用SELECT *,避免额外列拖慢性能 - 确保子查询里的关联条件带索引,否则全表扫描会非常慢
SELECT a.id, a.name FROM table_a a WHERE NOT EXISTS ( SELECT 1 FROM table_b b WHERE b.id = a.id AND b.status = a.status );
用 EXCEPT(或 MINUS)做集合差运算
EXCEPT 直接对比两组完整行,适合结构一致、字段顺序相同、且需要“完全相等”才视为重复的场景。PostgreSQL/SQL Server 支持 EXCEPT,Oracle 用 MINUS,MySQL 8.0+ 才支持 EXCEPT,老版本得绕路。
容易踩的坑:字段类型和顺序必须严格一致。比如 SELECT name, age 和 SELECT age, name 不能直接 EXCEPT;VARCHAR 和 TEXT 在某些引擎里会被判为不兼容。
实操建议:
- 先用
SELECT分别查出两张表的待比对字段,确认数据类型和长度是否对齐 - 加上
ORDER BY只影响输出顺序,不影响差集逻辑,但调试时加它更容易肉眼核对 - 如果某字段允许
NULL,EXCEPT会把NULL当作相等值处理——这和多数人直觉一致,但得心里有数
SELECT id, name, updated_at FROM table_a EXCEPT SELECT id, name, updated_at FROM table_b;
为什么不用 LEFT JOIN?或者什么时候能用
LEFT JOIN 看起来直观,但实际容易出错:一旦关联字段含 NULL,ON 条件就失效,导致本该被过滤的行意外保留;另外,如果表B对同一主键有多条记录,会产生笛卡尔膨胀,IS NULL 判断可能误判。
只有满足以下全部条件时,LEFT JOIN 才相对安全:
- 关联字段在表B中是主键或唯一约束(杜绝重复匹配)
- 关联字段在两张表中都
NOT NULL - 只比对单字段,或所有关联字段都确定无
NULL
否则,宁可多写一行子查询,也别贪图 JOIN 表面简洁。
大数据量时必须注意的性能点
差异核对不是简单 SELECT,而是隐式全表扫描+关联操作。千万级数据下,没索引的 NOT EXISTS 子查询可能跑十几分钟甚至超时。
关键动作只有两个:
- 给子查询中
WHERE的关联字段建联合索引,顺序要和ON条件一致(例如WHERE b.id = a.id AND b.status = a.status,索引应为(id, status)) - 避免在子查询里写
SELECT *或复杂表达式,尤其是函数调用(如UPPER(name)),会阻止索引使用
如果只是抽检,先用 LIMIT 100 验证逻辑,再删掉跑全量。别让一次误写的 NOT IN 把生产库拖垮。











