优先用 NOT EXISTS,慎用 NOT IN——因 NOT IN 遇子查询 NULL 时整条结果为空且无法走索引;NOT EXISTS 天然规避 NULL 问题,支持索引优化;LEFT JOIN + IS NULL 是直观替代方案。

直接说结论:优先用 NOT EXISTS,慎用 NOT IN —— 因为后者在子查询返回 NULL 时会整条结果变空,且无法利用索引加速。
NOT IN 为什么常出错
它不是“简单取反”,而是语义上要求子查询结果中**每个值都必须与主表值不等**。一旦子查询里有任意一个 NULL,整个 NOT IN 表达式就判定为 UNKNOWN,最终被当成 FALSE 处理,导致无记录返回。
- 常见错误场景:
SELECT * FROM orders WHERE customer_id NOT IN (SELECT id FROM customers WHERE region = 'US')—— 如果customers.id允许为NULL,哪怕只有一行是NULL,整个查询就查不到任何订单 -
NOT IN对索引不友好:数据库通常无法对子查询结果做高效哈希或排序匹配,尤其当右边是大表时,性能可能陡降 - 不能安全用于含聚合或复杂表达式的子查询(如
SELECT DISTINCT ...后再套NOT IN),容易触发隐式类型转换或空值陷阱
NOT EXISTS 的写法和优势
NOT EXISTS 是相关子查询,每次检查主表一行是否存在匹配,逻辑清晰、语义确定,且天然规避 NULL 问题。
- 标准写法:
SELECT * FROM orders o WHERE NOT EXISTS (SELECT 1 FROM customers c WHERE c.id = o.customer_id AND c.region = 'US') - 它只关心“是否存在”,不依赖子查询结果集的值本身,所以
c.id是否为NULL完全不影响外层判断 - 只要
customers(id)或customers(customer_id, region)有合适索引,优化器大概率走SEMI JOIN或索引查找,比NOT IN更可控 - 可自然扩展条件:比如加时间范围、状态过滤,都直接写在子查询的
WHERE里,不用额外处理空值
LEFT JOIN + IS NULL 作为替代方案
当你要查“左表有、右表无”的记录,且右表字段明确可空(如外键),LEFT JOIN ... WHERE right_table.pk IS NULL 是更直观、易调试的选择。
- 示例:
SELECT o.* FROM orders o LEFT JOIN customers c ON c.id = o.customer_id AND c.region = 'US' WHERE c.id IS NULL - 关键点:关联条件(
ON)里写右表的业务过滤(如c.region = 'US'),而空值判断必须放在WHERE,否则逻辑失效 - 别写成
WHERE c.region IS NULL—— 如果c.region是非空字段,这条件永远不成立;也别漏掉IS NULL写成= NULL,那永远没结果 - 执行计划里注意看是否用了索引:如果
c.id没索引,LEFT JOIN可能退化为嵌套循环,大数据量时要警惕
真正容易被忽略的是空值传播路径——不是你写的 NOT 不起作用,而是子查询或关联字段里藏着一个没意识到的 NULL,它会静默吞掉全部结果。动手前先 SELECT COUNT(*) FROM (...) AS sub WHERE col IS NULL 看一眼,比调半天执行计划更省时间。










