直接DELETE会撑爆日志,因每行删除均记完整undo/redo日志,千万行即千万条记录;必须分批提交(如TOP 10000+ORDER BY+显式COMMIT),否则单事务过长导致日志无法截断、锁表及报错9002或out of disk space。

为什么直接 DELETE FROM table 会撑爆日志文件
SQL Server 和 PostgreSQL 等数据库在执行 DELETE 时默认走完整事务日志记录:每删一行,日志里就记一条 undo/redo 记录。如果一次删几千万行,日志文件(tempdb 或 pg_xact 相关段)可能瞬间暴涨,甚至填满磁盘,导致事务失败或服务卡死。
常见错误现象包括:The transaction log for database 'xxx' is full、ERROR: out of disk space in pg_xlog,或者 SQL Server 报 9002 错误。
- 日志增长不是因为数据量大,而是因为单个事务太长、未提交
-
TRUNCATE TABLE虽快且日志少,但不能带WHERE条件,也不支持某些引擎(如 MySQL 的 InnoDB 在外键引用下受限) -
DROP + RECREATE不现实——你通常要保留表结构、索引、权限等
分批删除 + 显式事务控制(SQL Server / PostgreSQL)
核心思路:把大删拆成小事务,每批删完立刻提交,让日志可复用或自动截断。
以 SQL Server 为例:
WHILE (1=1)
BEGIN
DELETE TOP (10000) FROM <code>orders</code>
WHERE <code>created_at</code> < '2020-01-01';
<pre class='brush:php;toolbar:false;'>IF @@ROWCOUNT = 0 BREAK;
WAITFOR DELAY '00:00:00.01'; -- 防止 CPU 扛满END
关键点:
-
TOP (n)必须配合ORDER BY(如果需稳定顺序)或确保有高效索引支撑WHERE条件,否则每次扫描全表 - 每次
DELETE后立即提交(隐式或显式),避免长事务锁表+锁日志 -
WAITFOR DELAY不是必须,但在高并发写入场景下能缓解日志压力和锁竞争 - PostgreSQL 对应写法用
DELETE ... LIMIT n,但要注意:它不保证按索引顺序删,建议加USING子句或先SELECT ctid再删,避免重复或遗漏
用分区表或时间归档绕过 DELETE(MySQL / PostgreSQL)
如果你的数据天然按时间划分(比如日志、订单),最省心的方案根本不是删,而是“换表”。
- PostgreSQL 支持原生分区表(
PARTITION BY RANGE (created_at)),可直接ALTER TABLE ... DETACH PARTITION p_2020,然后DROP TABLE p_2020—— 这个操作几乎不写日志,秒级完成 - MySQL 8.0+ 分区表也支持
ALTER TABLE ... DROP PARTITION,同样低开销 - 即使没分区,也可提前建好按月表(
logs_202001,logs_202002…),写入时路由,清理时直接DROP TABLE
这种做法的前提是:业务能接受按时间粒度管理数据,且查询层做了路由适配(或统一视图封装)。
MySQL 的特殊处理:禁用 autocommit + 控制 innodb_log_file_size
MySQL InnoDB 的日志压力主要来自 ib_logfile 循环写入能力。如果批量删时频繁刷盘,可能卡住。
- 确保
autocommit = 0,手动控制事务边界,避免每行都 commit - 调大
innodb_log_file_size(例如从 48M → 512M),允许更大事务写入而不强制刷盘;但修改需停库重做日志文件 - 避免在
DELETE语句中使用子查询或函数(如DATE(created_at) ),会导致索引失效,全表扫描加重日志和 I/O 压力
真正容易被忽略的是:即使分批删,如果 WHERE 条件字段没索引,每次 TOP/LIMIT 都得扫全表——日志没少写,性能反而更差。先 EXPLAIN 看执行计划,比调任何参数都管用。

















