长事务中执行delete会撑爆undo空间,因每行删除都生成完整前镜像并全程保留至事务结束,非删得多而是删得久;须用order by+limit分批、建联合索引、禁用offset,并检查row_count()及时退出。

长事务中执行 DELETE 会直接导致 Undo 空间无法释放,不是因为删得多,而是因为“删得久”——只要事务没提交,所有被删行的前镜像(即 undo log)就必须全程保留。
为什么单条 DELETE 会撑爆 Undo 空间
MySQL InnoDB 的 DELETE 每删一行,都会在 undo log 中写入一份完整的“删除前镜像”。这不是元数据记录,而是逐行拷贝整行原始数据(含 TEXT/BLOB 字段)。删 50 万行,就生成 50 万份镜像;而这些镜像不会随事务推进逐步释放,必须等到整个事务 COMMIT 或 ROLLBACK 后,才由 purge 线程异步清理。
-
SHOW ENGINE INNODB STATUS\G中HISTORY LIST LENGTH> 5000 是明确预警信号 - 即使磁盘还有几十 GB 空闲,InnoDB 也会因无法分配新 undo slot 而拒绝写入,报
ERROR 1114或Undo log is full -
information_schema.INNODB_TRX中trx_rows_modified包含触发器产生的修改行数,但trx_query为空,容易误判为“空闲事务”
ORDER BY + LIMIT 分批删仍可能翻车
很多人以为加了 LIMIT 就安全,实际若不配合 ORDER BY 和索引,MySQL 可能跳过某些行、重复处理,甚至在并发写入时漏删。更糟的是:没 ORDER BY 的 DELETE ... LIMIT 不保证扫描顺序,优化器可能每次走不同索引路径,导致同一批数据反复尝试删除。
- 必须建联合索引,例如
INDEX idx_status_created (status, created_at),确保WHERE status = ? ORDER BY created_at能走索引扫描 - 禁用
OFFSET分页(如LIMIT 5000 OFFSET 10000),OFFSET 越大,扫描成本越高,且易漏数据 - 每批执行后检查
ROW_COUNT(),为 0 则立即退出,别再查COUNT(*)触发全表扫描
最容易被忽略的隐式放大点
业务逻辑里那些“看似只读”的操作,一旦落在事务内,就会拖长 Undo 生命周期。比如在 DELETE 事务中调用远程风控接口、查缓存、或执行 ORM 的 select_related,这些不写库的操作本身不产 undo,但会让事务挂起几十秒——这期间所有已删行的 undo 镜像都卡着不动,purge 线程完全无法介入。
- 触发器会把 DML 隐式绑进同一事务:主语句删 1 行,触发器再
UPDATE1000 行 → 这 1000 行的 undo 全算在同一个trx_id下 - 没加
EXCEPTION块的 PL/SQL 触发器出错(如ORA-01422),事务挂起但连接不释放,undo 持续占位 - MySQL 8.0.23+ 开启
innodb_undo_log_truncate=ON后,只要存在活跃大事务,truncate 就永远不会触发
真正卡住 Undo 的,从来不是那条 DELETE 语句本身,而是它所处的那个“你以为很快、其实已经悬停两分钟”的事务上下文——里面混着 RPC、循环、ORM 预加载、甚至一个忘记 COMMIT 的调试代码块。










