直接DELETE会卡死或爆日志,因其逐行写入完整事务日志(如SQL Server/PostgreSQL的ldf或pg_wal、MySQL的undo/redo log),百万行即百万日志条目,迅速撑满日志文件并持锁阻塞其他事务;根本解法是分批提交或改用TRUNCATE等低日志开销操作。

为什么直接 DELETE FROM table 会卡死或爆日志?
因为 DELETE 是逐行记录事务日志的操作,哪怕加了 WHERE 条件,SQL Server 或 PostgreSQL(启用 full recovery)也会为每条删除记录写入事务日志。百万行 ≈ 百万条日志条目,极易触发日志文件满、阻塞其他事务、锁表超时。
MySQL 的 InnoDB 同样受制于 undo log 和 redo log 压力;Oracle 的 rollback segment 也会快速膨胀。这不是性能问题,是事务日志容量和锁粒度的硬限制。
- 现象:执行十几秒后
DELETE卡住,sp_who2显示waittype = LCK_M_X或日志文件增长到上限报错The transaction log for database 'xxx' is full - 关键误区:加索引、拆 WHERE 条件、甚至加
WITH (ROWLOCK)都不能绕过日志量本质——只要走 DML 删除,就逃不开完整事务日志记录 - 真正出路:避开单一大事务,或彻底绕过日志机制(如分区切换、TRUNCATE)
用分批 DELETE + 显式事务控制日志峰值
适用于必须保留表结构、外键、索引、触发器,且无法停业务的场景。核心是「小事务 + 显式 COMMIT + 检查点间隙」。
以 SQL Server 为例(其他数据库逻辑类似,仅语法微调):
SET ROWCOUNT 10000;
WHILE (1=1)
BEGIN
BEGIN TRANSACTION;
DELETE FROM orders WHERE status = 'cancelled' AND created_at < '2022-01-01';
IF @@ROWCOUNT = 0
BEGIN
COMMIT;
BREAK;
END
COMMIT;
WAITFOR DELAY '00:00:01'; -- 防止 CPU 扛满,也给 checkpoint 缓冲时间
END
SET ROWCOUNT 0;-
SET ROWCOUNT控制每次最多删 1 万行,避免单次事务过大;务必在循环外重置 - 每次
DELETE后立刻COMMIT,释放日志空间和锁,而不是等整个循环结束 -
WAITFOR DELAY不是可选——它让 checkpoint 有机会把已提交事务的日志刷到磁盘,防止日志持续堆积 - 注意:WHERE 条件字段必须有高效索引,否则每次扫描全表,IO 爆增且锁范围扩大
用 TRUNCATE + 重建索引替代 DELETE(最快但有条件)
TRUNCATE TABLE 不记逐行日志,只记页释放操作,日志量极小,速度通常比批量 DELETE 快 5–10 倍。但它不是万能的:
- 不支持带
WHERE条件——只能清空整表 - SQL Server / PostgreSQL 中,若表被外键引用或参与分区方案,
TRUNCATE会失败 - MySQL 的
TRUNCATE会重置自增 ID,且在某些引擎(如 MyISAM)上不可回滚 - 如果只需删旧数据但保留新数据,可先
SELECT INTO新表,再DROP原表 +RENAME,但需短暂锁表
安全做法示例(SQL Server):
-- 1. 创建新表结构(含索引、约束) SELECT * INTO orders_new FROM orders WHERE 1=0; CREATE CLUSTERED INDEX IX_orders_new_created ON orders_new(created_at); <p>-- 2. 插入要保留的数据(建议用 TABLOCK 提升插入速度) INSERT INTO orders_new WITH (TABLOCK) SELECT * FROM orders WHERE created_at >= '2023-01-01';</p><p>-- 3. 切换(原子操作,毫秒级) DROP TABLE orders; EXEC sp_rename 'orders_new', 'orders';
分区表 + SWITCH 分区(SQL Server / PostgreSQL 10+)
这是真正面向百万级、千万级数据的工业级方案。前提是你建表时已按时间/ID做了分区(比如每月一个分区)。没分区?现在补代价很大,不推荐临时加。
- 删除 2022 年所有数据?只需对对应分区执行
ALTER TABLE ... SWITCH PARTITION x TO staging_table,再DROP staging_table -
SWITCH是元数据操作,不移动数据、不写日志、不锁全表,耗时基本恒定在毫秒级 - PostgreSQL 使用
DETACH PARTITION+DROP TABLE达到同样效果 - 陷阱:目标 staging 表结构必须与原分区完全一致(列名、类型、NULL 性、索引、约束),否则
SWITCH直接报错Msg 4945
分区不是银弹,但一旦用上,删旧数据就从“提心吊胆等两小时”变成“敲完回车喝口水”。没做前期设计,后面补分区迁移成本远高于批量 DELETE 的临时折腾。


















