NOT IN 在 DELETE 中会导致全表扫描、无法利用索引,且子查询含 NULL 时逻辑失效;应优先使用 NOT EXISTS,大数据量时改用 EXCEPT(SQL Server)或 LEFT JOIN ... IS NULL(MySQL)。

NOT IN 在 DELETE 中会触发全表扫描,且无法利用索引,尤其当子查询结果含 NULL 时逻辑直接失效。
NOT IN 的执行机制导致性能崩塌
SQL Server 和 MySQL 都不会把 NOT IN (subquery) 当作可下推的过滤条件优化。引擎实际执行步骤是:先完整执行子查询得到结果集(比如 SELECT id FROM backup_table),再对主表每一行,逐个比对该行字段值是否“不在这个结果集中”。这不是哈希查找,而是嵌套循环式判断 —— 没有索引参与,也没有短路机制。
更糟的是,只要子查询返回任意一个 NULL,整个 NOT IN 表达式就恒为 UNKNOWN,导致 WHERE 条件永远不成立,DELETE 一条都不删(这是 SQL 三值逻辑的坑,不是 bug)。
- 子查询返回 50 万行?主表扫描 + 50 万次逐行比对
- 子查询里有
NULL?DELETE 变成空操作,还锁表几十秒 - 字段没索引?连子查询本身都慢,主表扫描更是雪上加霜
NOT EXISTS 是更安全、更可控的替代方案
NOT EXISTS 不依赖子查询结果集的“存在性枚举”,而是对主表每行执行一次相关子查询(correlated subquery),一旦找到匹配就终止,天然支持索引下推。
例如:DELETE FROM orders WHERE NOT EXISTS (SELECT 1 FROM customers c WHERE c.customer_id = orders.customer_id)
- 如果
customers.customer_id有索引,每次子查询就是一次索引查找,复杂度接近 O(1) - 完全规避
NULL导致逻辑失效的问题 - 执行计划里能看到 Nested Loops + Index Seek,而不是 Hash Match + Table Scan
大数据量场景必须换用 EXCEPT 或 MERGE
当要删除“在 A 表但不在 B 表”的全部记录,且两表都超 10 万行时,NOT EXISTS 虽比 NOT IN 强,但仍是逐行驱动。此时应转向集合运算:
SQL Server 推荐写法:MERGE target USING (SELECT id FROM A EXCEPT SELECT id FROM B) AS src ON target.id = src.id WHEN MATCHED THEN DELETE;
-
EXCEPT自动去重、可并行、能走索引合并 -
MERGE是原子操作,避免中间状态和锁竞争 - 注意:MySQL 不支持
EXCEPT,得用LEFT JOIN ... IS NULL替代
真正容易被忽略的点是:哪怕你写了 NOT IN 并加了索引,只要子查询里有 GROUP BY、ORDER BY、或涉及可空列,优化器大概率放弃索引,退化为临时表 + 全表扫描。别赌优化器,直接换写法。


















