NOT EXISTS是最稳妥方式,语义明确且不受NULL影响,性能通常优于NOT IN;LEFT JOIN...IS NULL更直观但需确保用右表被连接字段判NULL;务必避免NOT IN因NULL导致结果为空。

用 NOT EXISTS 找出左表有但右表没有的记录
这是最常用也最稳妥的方式,尤其适合大表比对。数据库能利用右表的索引(如果存在)提前终止匹配,性能通常优于 NOT IN。
常见错误是忽略右表字段为 NULL 的情况:NOT IN 遇到右表任意值为 NULL 时整个条件返回 UNKNOWN,结果为空——而 NOT EXISTS 不受 NULL 影响。
- 写法示例:找出
orders表中客户 ID 在customers表里不存在的订单
SELECT o.* FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM customers c WHERE c.id = o.customer_id );
- 注意子查询里用
SELECT 1即可,不查实际字段;WHERE条件必须关联内外表(如c.id = o.customer_id),否则变成全量扫描 - 确保
customers.id或customers(customer_id)有索引,否则性能会断崖式下降
用 LEFT JOIN ... IS NULL 替代子查询更直观
当需要同时查看左表数据和“缺失依据”时,这种写法逻辑更直白,执行计划也常被优化器转成哈希连接,大数据量下可能更快。
- 同样以找“无效订单”为例:
SELECT o.* FROM orders o LEFT JOIN customers c ON o.customer_id = c.id WHERE c.id IS NULL;
-
LEFT JOIN后必须用WHERE c.id IS NULL,不能写c.id = NULL(SQL 中= NULL永远不成立) - 如果
customers.id是主键或非空列,用c.id IS NULL安全;但如果右表连接字段允许NULL,需确认业务上是否真要排除这些行 - 某些旧版 MySQL(5.6 及之前)对
LEFT JOIN ... IS NULL的索引选择不够智能,建议在生产环境先看EXPLAIN
为什么尽量避免 NOT IN?
它看起来简洁,但语义陷阱多,几乎只适合右表确定无 NULL 值且数据量小的场景。
- 一旦
customers.id中有任一NULL,整条NOT IN查询返回空结果集,极易导致线上漏数据 - 即使没
NULL,优化器也难对NOT IN (subquery)做有效下推,常触发临时表+全表扫描 - 示例(危险!):
SELECT * FROM orders WHERE customer_id NOT IN ( SELECT id FROM customers WHERE id IS NOT NULL );
- 上面加了
WHERE id IS NOT NULL看似规避问题,但子查询本身若因权限、过滤条件意外返回空集,NOT IN ()仍会返回全部左表数据——和预期完全相反
差集方向容易搞反:确认你要的是 A−B 还是 B−A
开发时经常顺手写错主表和从表位置。比如想查“哪些客户还没下过单”,实际却查了“哪些订单不属于任何客户”——两者语义完全不同。
- 口诀:
NOT EXISTS和LEFT JOIN的「主表」永远是你想保留数据的那个表 - 如果需求是“客户表中有、订单表中没有的客户”,主表就得是
customers,子查询或JOIN右侧才是orders - 复杂场景(如多字段联合判断差集),务必在
ON或子查询WHERE中严格对齐字段类型和空值处理逻辑,比如TEXT字段比较前是否要TRIM(),数字型 ID 是否有隐式转换风险
实际跑通前,先用 EXPLAIN 看一眼执行计划,重点盯住是否用了索引、有没有 Using temporary 或 Using filesort——差集操作很容易在不知不觉中变成全表嵌套循环。

















