not in易因子查询含null返回空集,应改用not exists或显式过滤null;not exists对null健壮、性能更优且支持索引,适合存在性判断。

子查询中用 NOT IN 实现“不包含A”但容易出错
直接写 SELECT * FROM t WHERE id NOT IN (SELECT id FROM a) 看似合理,但只要子查询结果里有任意一个 NULL,整条语句就返回空集——因为 expr NOT IN (NULL, ...) 在 SQL 中恒为 UNKNOWN,被当作 FALSE 处理。这是最常踩的坑,尤其当子查询来自 LEFT JOIN 或允许 NULL 的字段时。
实操建议:
- 务必在子查询中显式过滤 NULL:
SELECT id FROM a WHERE id IS NOT NULL - 或者改用
NOT EXISTS,它天然对 NULL 友好,语义也更清晰 - 避免在大表上对
NOT IN字段建索引失效的问题(MySQL 5.7+ 对含 NULL 的子查询优化仍有限)
用 NOT EXISTS + EXISTS 组合表达“不包含A但包含B”
集合差集不能直接写成 A - B,但可以拆解为:先找“包含B”的记录,再从中排除“也属于A”的记录。核心是把两个条件分别用独立的子查询表达,不耦合。
假设要查订单表 orders 中“未被标记为欺诈(不在 fraud_orders 表中),但已发货(在 shipped_records 表中有对应记录)”的订单:
SELECT o.* FROM orders o WHERE EXISTS ( SELECT 1 FROM shipped_records s WHERE s.order_id = o.id ) AND NOT EXISTS ( SELECT 1 FROM fraud_orders f WHERE f.order_id = o.id );
注意点:
-
EXISTS和NOT EXISTS都只关心子查询是否返回行,不关心内容,所以用SELECT 1最轻量 - 关联字段(如
s.order_id = o.id)必须有索引,否则性能会断崖式下降 - 不要写成
WHERE o.id IN (...) AND o.id NOT IN (...),IN/NOT IN 组合在 NULL 存在时逻辑不可控
用 LEFT JOIN ... IS NULL 替代 NOT EXISTS 的适用场景
当“不包含A”这部分需要同时取 A 表的其他字段(比如想查出“为什么没被标记为欺诈”的原因字段),LEFT JOIN 比 NOT EXISTS 更直观;但如果只是做存在性判断,NOT EXISTS 通常执行计划更优。
等价写法示例:
SELECT o.* FROM orders o INNER JOIN shipped_records s ON o.id = s.order_id LEFT JOIN fraud_orders f ON o.id = f.order_id WHERE f.order_id IS NULL;
关键差异:
-
LEFT JOIN ... IS NULL会在连接阶段生成临时中间集,若fraud_orders很大且无索引,内存和 IO 开销明显上升 - 如果
fraud_orders.order_id上没有索引,这个查询可能全表扫描,而NOT EXISTS在命中索引时可提前终止 - 当需要
f.reason这类字段用于后续判断时,只能选LEFT JOIN,此时记得加WHERE f.order_id IS NULL而不是AND f.order_id IS NULL(后者放错位置会导致逻辑错误)
为什么不用 MINUS 或 EXCEPT?
MySQL 直到 8.0.31 才支持 EXCEPT,且仅限于 UNION 级别语法糖,不能用于 WHERE 子句中的条件过滤;MINUS 完全不支持。所以想在条件中做集合差,只能靠子查询组合。
绕不开的现实限制:
- 无法像 Oracle 那样写
SELECT id FROM B EXCEPT SELECT id FROM A再嵌套进主查询 WHERE - 若硬要用派生表模拟差集,必须确保字段类型、NULL 性严格一致,否则
UNION会隐式转换或报错 - 多层子查询嵌套后,MySQL 的查询优化器可能放弃使用某些索引,建议用
EXPLAIN确认type是ref或eq_ref,而不是ALL
真正麻烦的不是写法,而是子查询的执行顺序和索引覆盖是否匹配——同一个语句,在小数据量下飞快,到线上百万级订单表就变慢,往往是因为关联字段缺索引,或者子查询被物化成了临时表。











