直接用FORALL删除大批量数据会引发UNDO爆满、锁表和实例变慢,因其不自动分事务;安全高效做法是结合BULK COLLECT获取ROWID、FORALL按批删除、每次后显式COMMIT。
直接用 forall 删除大批量数据,不加控制会爆 undo、锁表、拖慢整个实例——它只解决“批量发 sql”的效率问题,不解决“单次事务太大”的风险。真要安全高效删,得把 forall 和分批 + rowid + 显式 commit 绑在一起用。
为什么不能直接 FORALL 删除全表或大范围数据
FORALL 本身不自动分事务,它只是把 N 条 DML 合并成一次 SQL 引擎调用,但整个 FORALL 块仍在一个事务里。比如:
FORALL i IN 1..100000 DELETE FROM orders WHERE order_date <p>这看似“批量”,实则等价于一条超大 DELETE,照样占满 UNDO、触发 ORA-1555、阻塞其他会话查该表。真正起效的前提是:你先用 <code>BULK COLLECT INTO</code> 拿到一批 <code>ROWID</code>,再用 <code>FORALL</code> 按这批 <code>ROWID</code> 删,且每次循环后 <code>COMMIT</code>。</p><h3>正确写法:用 ROWID + BULK COLLECT + FORALL 分批删</h3><p>核心逻辑是:不依赖 WHERE 条件反复扫描表,而是先查出要删的物理地址(<code>ROWID</code>),再按地址精准删除。这样避免重复谓词计算、减少逻辑读,也方便控量。</p>
- 声明集合类型:
TYPE rid_array IS TABLE OF ROWID; - 每次最多取 10000 个
ROWID:SELECT rowid BULK COLLECT INTO rid_tab FROM t WHERE ... AND ROWNUM - 用
FORALL批删这批:FORALL i IN 1..rid_tab.COUNT DELETE FROM t WHERE rowid = rid_tab(i); - 立刻
COMMIT,释放 UNDO 和锁 - 检查
rid_tab.COUNT是否为 0,为 0 则退出循环
常见错误:WHERE 条件在 FORALL 里重复执行
下面这种写法看着简洁,实际极慢且不可控:
FORALL i IN id_list.FIRST..id_list.LAST DELETE FROM t WHERE id = id_list(i);
问题在于:
- 每次删都走索引查找或全表扫描,没复用执行计划
- 如果
id列无索引,等于做id_list.COUNT次全表扫描 - 无法限制单次影响行数,可能某次就删了 50 万行
- UNDO 增长不可预测,DBA 监控时看到的是“一个长事务”,不是“多个短事务”
换成 ROWID 方式,删多少行完全由 BULK COLLECT 的 ROWNUM 控制,物理定位快,且每轮都是确定的小事务。
性能和稳定性关键点
真实生产环境删千万级数据时,这几个细节决定成败:
-
BULK COLLECT必须加ROWNUM限制,否则内存溢出(尤其在低版本 Oracle) - 每次
COMMIT后建议加DBMS_LOCK.SLEEP(0.1),缓解日志写入压力 - 避免在高峰时段运行;如需在线删,考虑用
DBMS_SCHEDULER控制作业间隔 -
FORALL中不能用绑定变量以外的动态条件,例如DELETE WHERE status = :v_status AND rowid IN (...)是非法的 - 删完记得
ANALYZE TABLE ... COMPUTE STATISTICS或跑DBMS_STATS,否则后续执行计划可能劣化
最易被忽略的是:你以为用了 FORALL 就“自动优化”了,其实它只管发得快,不管事务大小。删多大、何时停、怎么释放资源,全得你手动掐住节奏。











