LEFT JOIN + WHERE IS NULL 是最通用的差集写法,因MySQL 8.0前不支持EXCEPT,NOT IN遇NULL会静默失效,而该写法逻辑清晰、兼容所有主流数据库,且必须用右表关联字段(如b.id)判NULL。

LEFT JOIN + WHERE IS NULL 是最通用的差集写法
MySQL 8.0 之前不支持 EXCEPT,NOT IN 在右表含 NULL 时会意外返回空结果,所以 LEFT JOIN 配合 WHERE ... IS NULL 是生产环境首选方案。它逻辑清晰、可读性强、兼容所有版本 MySQL 及 PostgreSQL、SQL Server、Oracle 等主流数据库。
关键点在于:左表全量保留,右表匹配失败的行在连接后对应字段为 NULL,再用 WHERE 筛出这些“未命中”的记录。
- 必须用
LEFT JOIN,不能用RIGHT JOIN(语义易反) - 判断条件必须是右表的关联字段(如
b.id)为NULL,不是左表字段 - 若右表本身有重复数据,不影响结果正确性;但若需严格按集合语义去重,应提前对右表
DISTINCT或加GROUP BY - 关联字段若有索引,性能接近最优;否则建议在
ON条件字段上建索引
为什么 NOT IN 容易出错
NOT IN 看似简洁,但在右子查询返回任意一个 NULL 值时,整个条件恒为 UNKNOWN,导致主查询无结果——这是 SQL 三值逻辑的典型陷阱。
例如:SELECT id FROM table_a WHERE id NOT IN (SELECT id FROM table_b),只要 table_b.id 中存在一个 NULL,整条语句就查不到任何数据,且不报错、无提示。
- 即使你确认右表没
NULL,一旦后续业务插入NULL,查询就静默失效 -
NOT IN子查询无法利用索引加速(尤其 MySQL),大数据量下比LEFT JOIN慢数倍 - 无法复用连接中的其他字段(比如你想同时拿到
table_a.name和table_b.updated_at的空值标记)
EXCEPT / MINUS 要看数据库版本和场景
MySQL 8.0+、PostgreSQL、SQL Server 支持 EXCEPT,Oracle 用 MINUS。它们语义明确、代码短,但有硬性前提:
- 两个查询必须字段数量相同、类型兼容、顺序一致(字段名可不同)
-
EXCEPT自动去重,若你本意是“取 A 中不在 B 中的所有行(含重复)”,它就不适用 - MySQL 5.7 及更早版本直接报错
ERROR 1064,不能 fallback - 执行计划中,
EXCEPT通常隐式触发排序+去重,大数据量下内存/临时表开销比LEFT JOIN更高
实际写法要注意字段别名和去重控制
如果你要取的是 A 表独有的完整记录(不止 ID),但 B 表只拿部分字段做比对,务必显式写出字段列表,避免因字段顺序或数量不一致报错。
示例:从 users 中找出没登录过 login_logs 的用户
SELECT u.id, u.email, u.created_at FROM users u LEFT JOIN login_logs l ON u.id = l.user_id WHERE l.user_id IS NULL;
若 login_logs 中 user_id 有重复,上面写法依然正确;但如果你想确保“每个用户只算一次”,可在 JOIN 前先聚合右表:
SELECT u.id, u.email FROM users u LEFT JOIN ( SELECT DISTINCT user_id FROM login_logs ) l ON u.id = l.user_id WHERE l.user_id IS NULL;
真正容易被忽略的,是字段兼容性检查——哪怕只是多一个 AS alias 导致左右字段名不一致,在 EXCEPT 场景下就会直接报错,而 LEFT JOIN 不受此限。

















