大表直接DELETE会导致锁表、生成大量undolog并阻塞DDL和MVCC清理;分批删除需用WHERE+ORDER BY+LIMIT配合单调索引字段、显式事务及状态记录,推荐存储过程实现。

大表直接DELETE为什么会导致锁表
MySQL(尤其是InnoDB)执行 DELETE FROM table WHERE ... 时,如果扫描行数多、事务未提交,会持续持有行锁甚至升级为间隙锁或表级意向锁;更关键的是,它会生成大量undolog,同时阻塞后续DDL(如 ALTER TABLE)和长事务的MVCC快照清理。线上大表删几百万行,可能让整个库响应变慢甚至夯住。
分批DELETE的核心控制点:WHERE + LIMIT + 显式事务
不能只靠 LIMIT,必须配合可重复定位的条件字段(通常是主键或带索引的单调字段),否则容易漏删或重复删。每次删除后要记录“已处理到哪”,下一批从该位置继续。
- 用
WHERE id > ? ORDER BY id LIMIT N替代WHERE status = 'old'这类无序条件 - 每次执行前加
BEGIN,删完立刻COMMIT,避免事务堆积 -
LIMIT值建议 1000–5000:太小导致循环次数多、网络开销大;太大仍可能触发锁升级或复制延迟 - 务必在
id字段上有索引,否则ORDER BY id会变成全表扫描
MySQL里怎么写安全的分批删除脚本
推荐用存储过程封装逻辑,避免应用层维护状态出错。下面是一个最小可行示例(假设按 id 删除旧数据):
DELIMITER $$ CREATE PROCEDURE batch_delete_old() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE last_id BIGINT DEFAULT 0; DECLARE batch_size INT DEFAULT 2000; <p>WHILE NOT done DO DELETE FROM orders WHERE id > last_id AND create_time </p><pre class="brush:php;toolbar:false;">IF ROW_COUNT() last_id AND create_time <p>END WHILE; END$$ DELIMITER ;</p>
注意:ROW_COUNT() 返回本次影响行数,是判断是否删完的关键;SLEEP() 不是必须,但在高负载库上能缓解压力;真实场景中,create_time 条件需确保有索引,否则 WHERE 部分会走全表。
比DELETE更轻量的替代方案:DROP PARTITION 或 TRUNCATE + INSERT
如果业务允许按时间/范围归档,分区表的 DROP PARTITION 是毫秒级、不锁表的操作;若无法改表结构,可考虑新建空表、INSERT符合条件的数据、再原子替换(RENAME TABLE)。这两种方式都绕开了逐行DML的锁和日志开销。
但要注意:DROP PARTITION 要求表必须是 RANGE/LIST 分区;RENAME 方案需要足够磁盘空间存两份数据,且中间有短暂不可写窗口。
真正难的不是写循环,而是确认「删哪些」的条件是否稳定、索引是否生效、以及监控每次循环的实际耗时与影响——这些往往比语法本身更容易被忽略。










