最稳妥的方式是用 NOT EXISTS,它能正确处理 B 表中关联字段为 NULL 的情况;而 NOT IN 遇 NULL 会因三值逻辑返回空结果,LEFT JOIN + IS NULL 是语义清晰的替代方案。

用 NOT EXISTS 找出只在 A 表存在、B 表不存在的记录
最稳妥的方式是用 NOT EXISTS,它能正确处理 B 表中关联字段为 NULL 的情况,而 NOT IN 在此场景下会意外返回空结果集。
假设要查 orders 表中有但 customers 表中没有对应 customer_id 的订单:
SELECT o.order_id, o.customer_id FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM customers c WHERE c.customer_id = o.customer_id );
-
NOT EXISTS子查询返回布尔值,不依赖字段是否为NULL - 子查询里写
SELECT 1是惯例,比SELECT *更轻量 - 必须给外层表加别名(如
o),并在子查询中显式引用,否则会报错或逻辑错误 - 如果
customers.customer_id没有索引,这个查询可能很慢——建议在关联字段上建索引
为什么 NOT IN 容易出错
当 customers.customer_id 中存在 NULL 值时,NOT IN 整个条件会恒为 UNKNOWN,导致结果为空——这不是 bug,而是 SQL 三值逻辑的必然行为。
下面这段看似等价的写法实际不可靠:
SELECT order_id, customer_id FROM orders WHERE customer_id NOT IN ( SELECT customer_id FROM customers );
- 只要子查询返回任意一个
NULL,整行就过滤掉了 - 即使你确认当前没
NULL,未来数据变更后也可能突然失效 - 如果子查询结果为空(
customers表为空),NOT IN ()也会返回空结果,而NOT EXISTS仍能正常返回全部orders
LEFT JOIN + IS NULL 是替代方案,但要注意连接条件
用左连接也能达到目的,语义清晰,执行计划也常被优化器友好对待,但必须确保 ON 条件准确,且判空字段来自右表。
SELECT o.order_id, o.customer_id FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id WHERE c.customer_id IS NULL;
- 必须用
c.customer_id IS NULL,而不是o.customer_id IS NULL——后者查的是订单本身缺客户 ID 的脏数据 - 如果连接字段类型不一致(比如一边是
VARCHAR一边是INT),隐式转换可能导致索引失效或意外匹配 - 多个连接条件时,所有关联字段都要出现在
ON中,不能挪到WHERE里,否则会退化成内连接
性能差异和索引建议
三种写法在小数据量下差别不大,但百万级以上数据时,执行计划和索引是否命中决定成败。
-
NOT EXISTS和LEFT JOIN通常都能利用customers(customer_id)上的索引做半连接(semi-join)或反连接(anti-join) -
NOT IN在某些旧版本 MySQL 中可能无法走索引,尤其当子查询含聚合或复杂表达式时 - 如果
orders.customer_id本身允许NULL,且你想排除这些记录,得额外加AND o.customer_id IS NOT NULL——NOT EXISTS和LEFT JOIN都不自动过滤它们
真正容易被忽略的是:子查询里哪怕只多一个无关字段、或漏掉表别名导致列歧义,都会让整个查询逻辑偏移。动手前先用 EXPLAIN 看一眼执行计划,比猜更可靠。

















