贝利信息

SQL 大表删除数据的正确方式

日期:2026-01-26 00:00 / 作者:冷漠man
千万级表直接DELETE易致日志暴涨、锁表、主从延迟甚至OOM;应分批删除(主键范围切片+显式COMMIT+限流)或重建表(导出保留数据+原子替换)。

大表 DELETE 为什么卡死或失败

直接对千万级以上的表执行 DELETE FROM table WHERE ... 极大概率导致事务日志暴涨、锁表时间过长、主从延迟飙升,甚至触发 OOM 或被 DBA 杀掉。根本原因在于:InnoDB 默认逐行加锁 + 记录完整 undo log,且整个语句是一个大事务,回滚段压力巨大。

常见现象包括:Lock wait timeout exceededQuery execution was interrupted、从库 SQL 线程长时间 Waiting for table metadata lock

分批删除的可靠写法(MySQL)

核心是控制每次删除的行数、显式提交、避开长事务。推荐用主键范围或自增 ID 切片,比 LIMIT 更稳定(LIMIT 在高并发更新下可能跳过或重复)。

示例:删除 orders 表中创建时间早于 2025 年的记录

SET @batch_size = 5000;
SET @low_id = 0;
SET @high_id = 0;

WHILE @high_id < (SELECT MAX(id) FROM orders WHERE created_at < '2022-01-01') DO SELECT MIN(id) INTO @low_id FROM orders WHERE id > @high_id AND created_at < '2022-01-01' LIMIT 1;

IF @low_id IS NULL THEN LEAVE; END IF;

SELECT MIN(id) INTO @high_id FROM orders WHERE id >= @low_id AND created_at < '2022-01-01' ORDER BY id DESC LIMIT 1;

DELETE FROM orders WHERE id BETWEEN @low_id AND @high_id AND created_at < '2022-01-01';

COMMIT; DO SLEEP(0.1); -- 控制节奏,减轻主从压力 END WHILE;

更安全的替代方案:改名 + 重建

当要删除的是历史归档数据(比如删掉 90% 以上),物理重建比逻辑删除更快、更干净,且几乎不锁原表。

步骤本质是「把要保留的数据导出 → 建新表 → 导入 → 原子替换」:

CREATE TABLE orders_new LIKE orders;
INSERT INTO orders_new SELECT * FROM orders WHERE created_at >= '2025-01-01';
RENAME TABLE orders TO orders_old, orders_new TO orders;

PostgreSQL 怎么办:用 DELETE ... WITH 分事务

PG 没有 LIMITDELETE 中的语法,也不能在单语句里用变量,推荐用 CTE + WITH 循环配合应用层控制。

典型做法是在应用代码里循环执行:

DELETE FROM logs 
WHERE id IN (
  SELECT id FROM logs 
  WHERE created_at < '2022-01-01' 
  ORDER BY id 
  LIMIT 10000
);

真正难的不是写出能跑的语句,而是判断「此刻该用分批删、还是重建、还是切归档库」——这取决于剩余数据比例、磁盘余量、主从延迟容忍度,以及你有没有权限停写。漏掉任一条件,都可能让操作从优化变成事故。