根本原因是单事务过长导致每行删除均生成完整前镜像并长期持锁;必须用ORDER BY+LIMIT或主键BETWEEN分批,配联合索引、ANALYZE TABLE,并清理隐形长事务以释放Undo。

大表 DELETE 操作产生海量 Undo Log,根本不是因为“删得多”,而是因为“删得长”——单事务锁住全量扫描行、为每一行保存完整前镜像,且这些镜像被长期钉住无法清理。
为什么DELETE不加LIMIT会触发Undo log full
MySQL InnoDB 默认把整条 DELETE FROM t WHERE ... 当作一个事务执行:每删一行,都必须在 Undo Log 中存一份“删除前的完整记录”。字段越多、越宽(尤其含 TEXT/JSON),单行镜像越大;百万行就是百万份镜像。常见报错包括:
Undo log is fullTransaction too large- 磁盘空间未满,但操作被强制中止
更隐蔽的问题是:扫描过程中所有匹配但尚未真正删除的行,都会持续持锁(gap lock 或 record lock),其他事务写入/读取被阻塞,形成连锁等待。
ORDER BY + LIMIT 是分批删除的底线要求
别信 DELETE ... LIMIT 5000 就安全——没 ORDER BY 时,优化器可能跳过某些行、或在并发写入下重复处理(尤其当 WHERE 条件无索引时)。正确姿势必须锁定扫描顺序:
- 确保
WHERE字段和排序字段有联合索引,例如INDEX idx_status_id (status, id) - 用子查询包裹
ORDER BY id LIMIT 5000,避免直接LIMIT被优化器绕过 - 示例:
DELETE FROM t WHERE id IN (SELECT id FROM (SELECT id FROM t WHERE status = 0 ORDER BY id LIMIT 5000) AS tmp) - 绝对不用
LIMIT 5000 OFFSET 10000:OFFSET 越大越慢,且易漏数据
BETWEEN主键区间比LIMIT更可控
高并发场景下,LIMIT 分页式删除仍有风险:新插入的行可能挤进下一批范围,导致漏删或重复删。主键连续区间更稳定:
- 先查边界:
SELECT MIN(id), MAX(id) FROM t WHERE status = 0 - 按步长切片,例如每次删
id BETWEEN 10001 AND 15000 - 步长建议 1000~5000;超过 10000 单次事务仍易触发 Undo 日志溢出
- 每次删完立刻执行
ANALYZE TABLE t,防止后续查询因统计信息陈旧而走错执行计划
真正卡住Undo释放的,往往不是DELETE本身
Undo Log 不是事务提交就消失的。它要等到所有可能用到该版本的事务都结束,才能被 Purge 线程回收。最常被忽略的卡点是:
- 一个未提交的调试连接(比如开发机上开着的 MySQL CLI,忘了
COMMIT或ROLLBACK) - 应用层异常中断后残留的空闲连接,
information_schema.innodb_trx中trx_started时间早于 DELETE 开始时间 -
innodb_max_undo_log_size过大(如默认 1GB)且innodb_undo_log_truncate=OFF,Undo 文件只增不缩
这些“隐形长事务”会让百万级 DELETE 产生的 Undo 持续占用磁盘数天——问题不在删得快慢,而在谁还盯着那些旧版本不放。


















