left join配合where is null是最可靠的方法,先左连接子表再筛选子表字段为null的记录,精准识别主表中无匹配项的行,避免not in遇null失效问题,且语义清晰、性能稳定。

用 LEFT JOIN 配合 WHERE IS NULL 最可靠
这是最常用也最不容易出错的方式。核心思路是:先左连接子表,再筛出子表字段为 NULL 的记录——说明主表某行在子表里没匹配项。
常见错误是写成 WHERE sub.id != NULL 或 WHERE sub.id NULL,这永远不成立,因为 NULL 不能用等号或不等号判断。
-
LEFT JOIN必须写在FROM子句里,不能用RIGHT JOIN反向写(逻辑易混乱) - 过滤条件
WHERE sub.column IS NULL必须放在JOIN之后,不能写进ON子句(否则会变成内连接效果) - 如果子表有多个关联字段,任选一个非空约束字段判
IS NULL即可,比如主键或外键字段
SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL;
NOT EXISTS 比 NOT IN 更安全
当子表可能含 NULL 值时,NOT IN 会意外返回空结果集——因为 value NOT IN (1, 2, NULL) 永远为 UNKNOWN,被当作 FALSE 过滤掉。
NOT EXISTS 没这个问题,它只关心子查询是否返回行,不依赖值比较。
-
NOT EXISTS子查询必须相关(correlated),即要引用外部主表字段,如WHERE o.user_id = u.id - 性能上,现代优化器对
NOT EXISTS和LEFT JOIN ... IS NULL通常生成相似执行计划,但语义更清晰 - 避免写
NOT IN (SELECT user_id FROM orders)—— 若orders.user_id允许NULL,整条语句就失效
SELECT * FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id );
子查询用 NOT IN 时必须排除 NULL
只有确认子表关联字段 **绝对不允许 NULL**,才考虑 NOT IN。即便如此,仍建议加显式过滤以防 schema 变更引入风险。
例如 orders.user_id 是外键且设了 NOT NULL 约束,看似安全,但后期若约束被删或数据异常,查询就静默失败。
- 强制加
WHERE user_id IS NOT NULL来兜底 - 注意
NOT IN对空子查询结果会返回全部主表数据(符合预期),但和NULL混用就不可靠 - MySQL 8.0+、PostgreSQL、SQL Server 都有此行为,不是方言差异,是 SQL 标准定义
SELECT * FROM users WHERE id NOT IN ( SELECT user_id FROM orders WHERE user_id IS NOT NULL );
性能关键点:索引和 NULL 处理方式
三种写法在大多数场景下性能接近,但瓶颈往往不在语法本身,而在索引缺失或 NULL 引发的全表扫描。
比如 LEFT JOIN ... WHERE sub.id IS NULL 如果 sub.id 没索引,数据库可能放弃使用索引而走嵌套循环;NOT EXISTS 则更倾向用半连接(semi-join)优化。
- 确保子表关联字段(如
orders.user_id)有索引,尤其是NOT EXISTS和LEFT JOIN场景 -
NOT IN子查询若无索引,还可能触发临时表 + 文件排序,比前两种更慢 - PostgreSQL 中
NOT EXISTS对部分复杂子查询支持更好的并行计划;MySQL 则对LEFT JOIN优化更成熟
实际用哪一种,取决于你是否能控制子表字段的 NULL 性、团队习惯、以及执行计划是否稳定——别只看写法“看起来简洁”,得看 EXPLAIN 输出里有没有 Using where; Not exists 或 Null-aware anti join 这类关键提示。











