逐行循环在Oracle中特别慢,因每次FETCH和UPDATE都触发PL/SQL与SQL引擎间高开销上下文切换,导致CPU占用高、I/O串行化;BULK COLLECT+FORALL通过批量移交数据避免切换,提升性能。
为什么逐行循环在Oracle里特别慢
因为每次 fetch 单行、每次 update 单行,都会触发一次 pl/sql 引擎和 sql 引擎之间的上下文切换。这种切换不是“快进快出”,而是涉及内存上下文保存、执行环境重建、游标状态维护——实测中,10 万次切换能吃掉 70% 以上的 cpu 时间。更麻烦的是,它还会把原本可以并行的 i/o 变成串行,让索引和 buffer cache 失去优化空间。
BULK COLLECT + FORALL 是怎么绕过切换的
核心在于“批量移交”:PL/SQL 引擎一次性把整批数据(比如 1000 行)打包成数组,直接交给 SQL 引擎处理;SQL 引擎内部用类似单条带数组参数的语句完成全部 DML,全程不回调 PL/SQL 层。
-
BULK COLLECT INTO必须配合LIMIT使用,否则大结果集会直接撑爆 PGA,报ORA-04030 -
FORALL i IN 1..v_data.COUNT的下标必须连续;若中间有空值或删过元素,得改用INDICES OF v_data -
FORALL里不能写函数调用(如UPPER(v_data(i).name)),所有计算必须在FORALL外提前做好 - 更新条件必须用
ROWID或主键,避免二次查询——WHERE id = v_data(i).id比WHERE code = v_data(i).code更稳
常见报错和对应修复点
ORA-06502:集合类型声明和实际字段不匹配,比如 VARCHAR2(10) 字段塞进了 15 字节值;或者集合为空却没判 v_data.COUNT = 0 就进 FORALL。
ORA-01403:出现在 FORALL ... SAVE EXCEPTIONS 里没检查 SQL%BULK_EXCEPTIONS.COUNT,导致异常被忽略后下一条继续报错。
- 别依赖
%NOTFOUND判断BULK COLLECT是否结束,要用v_data.COUNT = 0 - 集合类型优先选
TABLE OF ...(嵌套表),别用VARRAY(长度固定易溢出)或关联数组(某些 DML 不支持绑定) -
SAVE EXCEPTIONS后必须立刻查SQL%BULK_EXCEPTIONS,错误编号是SQL%BULK_EXCEPTIONS(i).ERROR_CODE,不是SQLERRM
什么时候不该用 BULK COLLECT + FORALL
当单次 BULK COLLECT 获取的数据量超过 5 万行,且 PGA 内存受限(比如 OLTP 系统限制为 256MB),强行全量加载反而引发频繁的磁盘交换。这时候要退回到分批循环,但每批仍坚持用 BULK COLLECT LIMIT 1000 + FORALL,而不是退化成逐行 FETCH。
还有就是逻辑强依赖上一行结果时(比如滚动累加、状态机流转),硬套批量会破坏语义——这种场景不如老老实实用游标 FOR rec IN (...) LOOP,再配合 MOD(i, 100) = 0 批量提交。











