not in 遇 null 会导致 delete 不生效,因 x not in (..., null) 中 x != null 恒为 unknown;应过滤 null 或改用 not exists(需相关子查询);大表或含 null 时优先 not exists,且需确保索引;删前务必检查 null、执行计划并用 select 预演。

NOT IN 删除时 NULL 值会导致整条语句不生效
这是最常踩的坑:当子查询返回的结果里包含 NULL,NOT IN 会直接返回空结果集,DELETE 一条都不删。因为 x NOT IN (1, 2, NULL) 在 SQL 中等价于 x != 1 AND x != 2 AND x != NULL,而 x != NULL 永远是 UNKNOWN,整个条件就失效了。
实操建议:
- 先检查子查询是否可能返回
NULL:SELECT COUNT(*) FROM t2 WHERE id IS NULL;
- 若存在
NULL,必须显式过滤:DELETE FROM t1 WHERE id NOT IN (SELECT id FROM t2 WHERE id IS NOT NULL);
- 或者改用
NOT EXISTS,它天然规避这个问题
NOT EXISTS 的正确写法是相关子查询,不是简单套用
NOT EXISTS 必须搭配相关子查询(correlated subquery),即子查询里要引用外层表的字段;否则逻辑就错了,可能误删或漏删。
实操建议:
- 错误写法(非相关,等效于全表扫描+布尔常量):
DELETE FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.id = 123);
- 正确写法(关联外层
t1.id):DELETE FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);
- 注意子查询中
SELECT后面的内容无关紧要,SELECT 1、SELECT NULL效果一样,但别写SELECT *(无意义且可能拖慢)
性能差异大:小表用 NOT IN,大表或含 NULL 时优先 NOT EXISTS
NOT IN 通常会被优化为哈希 Anti-Join,但前提是子查询结果确定、无 NULL;一旦有 NULL 或子查询复杂,优化器可能退化为嵌套循环,性能断崖下跌。而 NOT EXISTS 在绝大多数引擎(MySQL 8.0+、PostgreSQL、SQL Server)中都能稳定走索引 + Semi/Anti-Join。
实操建议:
- 确认
t2.id上有索引,否则NOT EXISTS也会很慢 - 在 PostgreSQL 或 SQL Server 中,可加
EXPLAIN看执行计划,重点找Anti Join或Not Exists节点 - MySQL 5.7 及更早版本对
NOT IN优化较弱,含子查询时尤其容易全表扫描,直接换NOT EXISTS
DELETE … USING(PostgreSQL)或 JOIN(MySQL)是更直观的替代方案
虽然问题问的是 NOT IN 和 NOT EXISTS,但实际删除冗余行时,用显式连接往往更可控、易读、易调试。
实操建议:
- PostgreSQL 推荐写法:
DELETE FROM t1 USING t2 WHERE t1.id = t2.id;
(删掉所有在t2中存在的t1行) - MySQL 8.0+ 支持:
DELETE t1 FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t2.id IS NULL;
- 这类写法避免了子查询语义绕弯,也天然绕开
NULL陷阱,调试时把DELETE换成SELECT *就能预览要删哪些行
真正容易被忽略的不是语法,而是删前没确认子查询结果集是否含 NULL、没看执行计划、也没做 SELECT 预演 —— 这三步跳过,删库跑路只差一个回车。










