单事务批量dml会炸undo表空间,因oracle按数据块旧镜像而非行存undo,扫描多行可能仅改少量块但需存储完整块镜像;异常未提交或回滚时事务挂起持续占用undo。

为什么单事务批量DML会炸掉UNDO表空间
Oracle写UNDO不是按“行”存,而是按“数据块变更前镜像”存。一条UPDATE扫50万行,可能只改了几十个数据块,但每个块的完整旧镜像都要记进UNDO段——瞬间占满UNDOTBS1,触发ORA-30036。更糟的是,如果过程里有异常分支没COMMIT或ROLLBACK,事务就挂着不动,持续锁死UNDO块。
用FORALL + BULK COLLECT分片提交最稳
别手写循环+COMMIT,容易漏控制、难调试。优先用PL/SQL原生批量机制:
-
FORALL i IN 1..batch_size SAVE EXCEPTIONS能自动捕获失败行,不因个别报错中断整批 - 配合
BULK COLLECT INTO一次取几千行,避免游标全量加载内存爆掉 - 每批执行后立刻
COMMIT,释放对应UNDO段,让空间可复用
示例关键片段:
DECLARE
TYPE id_tab IS TABLE OF employees.employee_id%TYPE;
v_ids id_tab;
CURSOR c IS SELECT employee_id FROM employees WHERE dept_id = 10;
BEGIN
OPEN c;
LOOP
FETCH c BULK COLLECT INTO v_ids LIMIT 5000;
EXIT WHEN v_ids.COUNT = 0;
<pre class="brush:php;toolbar:false;">FORALL i IN 1..v_ids.COUNT SAVE EXCEPTIONS
UPDATE employees SET salary = salary * 1.1 WHERE employee_id = v_ids(i);
COMMIT; -- 每5000行就落地一次END LOOP; CLOSE c; END;
警惕PRAGMA AUTONOMOUS_TRANSACTION和隐式大事务
这两个是生产环境最常见的UNDO黑洞:
-
PRAGMA AUTONOMOUS_TRANSACTION在子过程中开独立事务,但父过程仍挂着——子过程COMMIT了,父过程UNDO照锁不误 - 存储过程里没写
EXCEPTION块,或写了但分支里漏了ROLLBACK,异常一出,事务就卡死在V$TRANSACTION里,USED_UBLK持续上涨 - 查长事务用:
SELECT s.sid, s.serial#, t.used_ublk, t.start_time FROM v$transaction t JOIN v$session s ON t.ses_addr = s.saddr WHERE t.used_ublk > 10000 AND t.start_time (30分钟以上)
分批逻辑必须绕开ROWID伪列陷阱
有人用WHERE ROWID BETWEEN ... AND ...分片,看似简单,实则危险:
- 表发生
MOVE、SHRINK或分区SPLIT后,ROWID会变,分片逻辑直接失效 - 高并发下,不同批次可能重复处理同一行(尤其没加
SELECT FOR UPDATE时) - 真正安全的分片键是业务主键(如
id)或时间字段(如create_time),配合MIN/MAX边界控制
UNDO压力从来不是靠调大UNDO_RETENTION能缓解的——它只是个建议值,空间真紧张时Oracle照样覆盖旧数据。分批的核心,是让事务生命周期足够短,从根上掐断UNDO锁死链。











