一次性DELETE大表会卡死数据库,因InnoDB逐行加锁、写undo log、更新二级索引,全表扫描导致IO和CPU过载,IO放大3–5倍;应分段删除(如WHERE id BETWEEN)、用pt-archiver工具控制节奏。

为什么一次性 DELETE 大表会卡死数据库
因为 InnoDB 默认在事务中逐行加锁、写 undo log、更新二级索引,全表扫描+逐行删除时,IO 和 CPU 都被锁死,还可能触发长事务阻塞其他查询。尤其当目标行分散在多个数据页、且有多个二级索引时,实际 IO 放大 3–5 倍不止。
- 别信“加了主键索引就快”——
WHERE条件没走索引,照样全表扫描 - 别在高峰期跑
DELETE FROM t WHERE status = 0,哪怕加了status索引,如果该值选择性差(比如 90% 行都匹配),优化器仍可能放弃索引 - MySQL 8.0+ 的
innodb_redo_log_capacity不够大时,大批量删除还会频繁刷 redo,进一步拖慢 IO
用 WHERE id BETWEEN ? AND ? 分段删比 LIMIT 更稳
LIMIT 在高并发写入场景下容易漏删或重复删:因新插入的行可能挤进已查范围,而 BETWEEN 基于主键连续区间,逻辑确定、可预测、支持断点续删。
- 先查出最小和最大
id:SELECT MIN(id), MAX(id) FROM t WHERE status = 0 - 每次删固定步长(如 5000 行):
DELETE FROM t WHERE status = 0 AND id BETWEEN 10001 AND 15000 - 步长别设太大(>10000):单次事务日志膨胀、锁持有时间过长;也别太小(
- 务必在
status和id上建联合索引:INDEX idx_status_id (status, id),否则BETWEEN无法高效定位
删完立刻 ANALYZE TABLE,但别急着 OPTIMIZE TABLE
删除大量行后,InnoDB 的统计信息不会自动更新,可能导致后续查询选错执行计划;而 OPTIMIZE TABLE 本质是重建表,在大表上会锁表、IO 爆增,多数情况没必要。
-
ANALYZE TABLE t轻量、秒级完成,强制刷新索引基数,应作为分段删除每轮后的固定动作 -
OPTIMIZE TABLE只在满足以下任一条件时考虑:DATA_FREE占比 > 25%(查SHOW TABLE STATUS LIKE 't')、或有大量碎片导致SELECT明显变慢 - MySQL 5.7+ 开启
innodb_file_per_table=ON后,OPTIMIZE不会回收系统表空间,只整理单表空间文件
真正省 IO 的做法:用 pt-archiver 替代手写脚本
自己写 while 循环 + SLEEP 容易误判进度、忽略连接超时、没法自动跳过锁冲突行;而 pt-archiver 内置游标式扫描、失败重试、延迟控制、低峰期限流,IO 更平滑。
- 基础命令:
pt-archiver --source h=localhost,D=db,t=t --where "status=0" --limit 5000 --bulk-delete --purge --progress 10000 -
--bulk-delete让它用WHERE id IN (…)批量删,比单条DELETE减少网络和解析开销 -
--sleep 0.1控制每批间隔,避免打满 IO;--no-check-charset可跳过字符集校验(内网可信环境) - 注意:它默认不走索引提示,如果执行计划异常,得加
--where "status=0 AND id > ?" --order-by "id"强制索引扫描
分段逻辑本身不难,难的是对索引失效的敏感度、对统计信息滞后的预判、以及把工具链压进业务低谷期——这些地方一松劲,IO 就反弹。

















