分批DELETE+COMMIT是最优解,关键在于合理分片:MySQL用ORDER BY主键+LIMIT并记录断点;PostgreSQL用ctid子查询或WITH;Oracle用ROWID而非ROWNUM;所有场景均需确保WHERE字段有索引。
DELETE 太大导致 UNDO 表空间爆满怎么办
直接分批 delete + commit 是最常用也最有效的解法,但关键不在“分不分”,而在“怎么分”——分得太大照样撑爆 undo,分得太小又拖慢速度、加重日志压力。
用 WHERE + LIMIT 分批删(MySQL / PostgreSQL)
别用 SELECT * FROM t WHERE ... 先查再删,那是纯浪费内存和网络。直接在 DELETE 里带条件和行数限制,靠主键或索引字段推进。
- MySQL:用
DELETE FROM t WHERE status = 'old' ORDER BY id LIMIT 10000,每次删完记下最后id,下轮改WHERE id > ? AND status = 'old' - PostgreSQL:不支持
LIMIT在DELETE里直接用,得套一层WITH或用游标;更稳的是DELETE FROM t WHERE ctid IN (SELECT ctid FROM t WHERE status = 'old' ORDER BY ctid LIMIT 10000) - 注意:没索引的
WHERE条件会导致全表扫描,每批都变慢,甚至锁表;务必确认status、created_at等过滤字段有索引
Oracle 用 ROWID 分批删,别碰 ROWNUM
ROWNUM 是结果集生成时动态分配的,用在 DELETE 里会误删(比如 WHERE ROWNUM 实际可能只删几十行),必须用 <code>ROWID 做稳定锚点。
- 先查一批
ROWID:SELECT ROWID FROM t WHERE status = 'old' AND ROWNUM - 再删:
DELETE FROM t WHERE ROWID IN (SELECT ROWID FROM t WHERE status = 'old' AND ROWNUM - 或者更安全:把
ROWID存临时表,用BULK COLLECT+FORALL批量绑定删除,避免硬解析开销 - UNDO 消耗和单次事务内修改的块数强相关,10000 行若分散在 500 个数据块,比集中在 50 个块里更吃 UNDO
分批大小不是拍脑袋定的
1000、10000、50000 这些数字背后是 UNDO 段配置、块大小、行平均长度的真实约束,不是越大越好,也不是越小越稳。
- 估算单批最大事务大小:
avg_row_length × batch_size ≤ db_block_size × 0.7(留 30% 给 ITL 和其他开销) - Oracle 查当前 UNDO 表空间上限:
SELECT tablespace_name, status, retention FROM dba_tablespaces WHERE contents = 'UNDO';如果retention是GUARANTEE,且 UNDO 表空间不能自动扩展,那 batch_size 必须压得更低 - MySQL 的
innodb_log_file_size也卡着单事务能写多少 redo;超了会触发强制 checkpoint,拖慢整体速度 - 实测建议:从 1000 行起步,在业务低峰跑几轮,看
SHOW ENGINE INNODB STATUS里的History list length是否持续上涨;涨得快就说明 batch 太大或 COMMIT 频率太低
真正容易被忽略的,是删除后索引维护的隐性成本——大批量删会导致 B+ 树大量页合并与分裂,后续 INSERT/UPDATE 可能变慢;如果删的是历史冷数据,考虑分区表 + DROP PARTITION,比 DELETE 干净得多。

















