本质不是“删得慢”,而是锁、日志、主从三重阻塞叠加:RR级别下全表扫描触发next-key lock,undo/binlog膨胀阻塞purge线程,从库SQL线程串行回放导致延迟飙升。

大事务导致的物理删除慢,本质不是“删得慢”,而是锁、日志、主从三重阻塞叠加的结果;直接上 LIMIT 或关 autocommit 不解决问题,反而可能漏删、卡死或拖垮从库。
为什么 DELETE 百万行会卡在 Updating 状态?
MySQL 8.0 默认 RR 隔离级别下,DELETE FROM t WHERE status = 'done' 若没走索引或匹配行过多,InnoDB 会升级为全表扫描 + next-key lock —— 相当于给整张表加锁。同时每行变更都写 undo log 和 binlog,purge 线程跟不上,History list length 持续飙升,后续所有读写都被拖慢。
- 现象:执行后
SHOW PROCESSLIST显示Updating,持续数分钟不退 - 确认方式:查
information_schema.INNODB_TRX,看TRX_ROWS_MODIFIED是否 >10000 且TRX_STARTED超过 5 分钟 - 别信
EXPLAIN的rows估算值——真实扫描量可能翻几倍,尤其当status基数低(如只有两个值)时,优化器常放弃索引
分批删必须满足的三个硬条件
不是“加个 LIMIT 就叫分批”,漏掉任一条件都会导致删不完、重复删、或性能更差。
- WHERE 条件必须命中联合索引最左前缀,例如
(status, id),且id是单调递增主键;只建INDEX(status)不够,因为分页时无法快速定位下一批起点 - 每次必须带
ORDER BY id LIMIT N,否则 InnoDB 不保证扫描顺序,高并发下同一批可能反复处理同一段id区间 - 不能用
LIMIT offset, N——offset越大,MySQL 越可能放弃索引,变成“跳过 offset 行再取 N 行”,底层仍是全表扫
如何安全执行分批删并避免主从延迟?
主库删得快,不代表从库跟得上。一个 50 万行的事务在主库提交只需 3 秒,但从库 SQL 线程要逐条回放,期间 Seconds_Behind_Master 会跳涨几十秒,且并行复制(slave_parallel_workers)对单事务完全无效。
- 每批独立事务:显式
START TRANSACTION; ... ; COMMIT;,禁用autocommit,否则每行都刷 binlog,IOPS 爆表 - 控制单批大小:建议
LIMIT 1000–5000,视单行大小和索引效率调整;超过 1 万行易触发长事务告警和锁等待 - 应用层加休眠:每批
COMMIT后SLEEP(0.1–0.5),缓解从库追赶压力,也防主库Threads_running突增 - 监控关键指标:实时盯
Innodb_row_lock_waits和Slave_SQL_Running_State,若出现Reading event from the relay log卡住,说明 relay log 写入或 SQL 回放已成瓶颈
什么情况下该放弃 DELETE,改用 TRUNCATE 或分区删除?
如果目标是清空历史数据(比如日志表、事件表),且满足约束条件,TRUNCATE 或 DROP PARTITION 是数量级提升的解法,而不是“高级技巧”。
-
TRUNCATE TABLE logs_2023比等量DELETE快 10–100 倍,不走事务、不写 row-based binlog、立刻释放磁盘空间,但要求:无外键引用、可接受自增 ID 重置、有DROP权限 - 分区表场景优先用
ALTER TABLE t DROP PARTITION p2023:秒级完成,不影响其他分区,也不产生 undo/redo 压力;需提前按时间字段建 RANGE/LIST 分区 - 别在业务高峰期执行
TRUNCATE或DROP PARTITION:虽不锁表,但会触发元数据锁(MDL),阻塞后续 DDL 和部分 DML
真正容易被忽略的点是:分批删的“锚点字段”必须全局单调且有索引,而不仅仅是“看起来有序”。很多线上事故源于用 created_at 当分页依据——高并发写入下,毫秒级时间戳重复、乱序,导致某批数据永远无法被覆盖。用 id 最稳妥,但前提是它真能代表写入顺序,且没有批量导入导致的跳跃。


















