子查询删除必须用EXISTS而非IN,因NOT IN遇NULL返回UNKNOWN导致误删;正确写法为DELETE FROM t WHERE NOT EXISTS (SELECT 1 FROM ref WHERE ref.id = t.id),且需先SELECT验证、加索引、分批执行。

子查询删除必须用 EXISTS 而不是 IN
直接用 DELETE FROM main_table WHERE id NOT IN (SELECT id FROM ref_table) 在大多数数据库里会出错或删错——尤其当 ref_table.id 包含 NULL 时,NOT IN 整个条件直接返回 UNKNOWN,结果一行都不删。这是最常踩的坑。
- 用
EXISTS替代:它不受NULL影响,语义清晰(“不存在匹配记录”) - 写法示例:
DELETE FROM orders WHERE NOT EXISTS ( SELECT 1 FROM customers WHERE customers.id = orders.customer_id );
- 注意子查询里必须关联外层表(如这里的
customers.id = orders.customer_id),否则变成全量扫描+笛卡尔积
MySQL 不支持在子查询中直接删被引用的表
执行 DELETE FROM t1 WHERE id IN (SELECT id FROM t1 WHERE ...) 会报错 You can't specify target table 't1' for update in FROM clause。这不是语法问题,是 MySQL 的限制。
- 绕过方法:把子查询包装成派生表(加一层
SELECT * FROM (...) AS tmp) - 正确写法:
DELETE FROM orders WHERE id IN ( SELECT id FROM ( SELECT o1.id FROM orders o1 LEFT JOIN customers c ON o1.customer_id = c.id WHERE c.id IS NULL ) AS tmp ); - PostgreSQL 和 SQL Server 没这个限制,但为兼容性建议统一用 EXISTS 写法
删除前务必加 WHERE 条件并先查后删
子查询删除容易误删全表,尤其关联条件写错或索引缺失时,性能和安全性都危险。
- 第一步永远先运行等价的
SELECT语句,确认要删的数据量:SELECT COUNT(*) FROM orders WHERE NOT EXISTS ( SELECT 1 FROM customers WHERE customers.id = orders.customer_id );
- 第二步检查
orders.customer_id和customers.id是否有索引——没索引时子查询可能扫全表,几百万行就卡死 - 生产环境禁止不带
LIMIT的批量删(MySQL 支持LIMIT,PostgreSQL 需用 CTE 分批)
PostgreSQL 中用 CTE 实现安全分批删除
单次删太多行会锁表、写 WAL 过大、触发 autovacuum 压力。PostgreSQL 没有 LIMIT 删除语法,得靠 CTE 控制节奏。
- 分批删 1000 行示例:
WITH batch AS ( SELECT id FROM orders WHERE NOT EXISTS ( SELECT 1 FROM customers WHERE customers.id = orders.customer_id ) ORDER BY id LIMIT 1000 ) DELETE FROM orders WHERE id IN (SELECT id FROM batch); - 重复执行直到
DELETE返回0 rows affected - 别用
OFFSET分页删——数据变动时会漏删或重删
实际执行时,EXISTS 的性能通常比 IN 更稳定,但具体快慢取决于索引是否覆盖关联字段;而 CTE 分批的关键不是“看起来优雅”,是避免长事务阻塞其他操作。

















