BULK COLLECT + FORALL 可将批量 UPDATE 从几小时优化至几分钟,关键在于正确选择集合类型、合理分页及严谨异常处理。

直接用 BULK COLLECT + FORALL 能把批量 UPDATE 从几小时压到几分钟,但前提是集合类型选对、分页控好、异常真去查——否则反而比普通循环更难调试。
为什么逐行 UPDATE 在 PL/SQL 里特别慢
PL/SQL 引擎和 SQL 引擎之间每次执行 UPDATE 都要切换上下文(context switch),10 万次循环 = 10 万次切换。而 FORALL 把这 10 万次 DML 合成一次批量提交,底层只做一次解析、一次执行、一次返回结果。
- 常见错误现象:
ORA-06502多半是集合为空或字段类型不匹配;ORA-01403出现在FORALL ... SAVE EXCEPTIONS里没检查SQL%BULK_EXCEPTIONS就直接报错中断 - 别在
FORALL内部写表达式,比如SET col = func(x)—— 所有计算必须在FORALL外完成,否则失去批量意义 - 游标属性如
%NOTFOUND在BULK COLLECT后不可靠,要用v_tab.COUNT = 0判断是否取完
BULK COLLECT INTO 必须配对正确集合类型
BULK COLLECT 的目标变量只能是集合(TABLE OF / INDEX BY),不能是标量或记录变量;且类型必须严格对应 SELECT 字段顺序与精度。
- 优先选
TYPE t_tab IS TABLE OF your_table%ROWTYPE,别用VARRAY(长度固定易溢出)或纯INDEX BY PLS_INTEGER(某些 DML 场景不支持绑定) - 如果只更新部分字段,别用
%ROWTYPE,改用显式字段集合:TYPE t_ids IS TABLE OF employees.employee_id%TYPE;TYPE t_sals IS TABLE OF employees.salary%TYPE;然后SELECT emp_id, sal BULK COLLECT INTO v_ids, v_sals FROM ... - 用
FETCH ... BULK COLLECT INTO ... LIMIT 1000显式分页,不依赖默认行为(默认无上限,容易 OOM)
FORALL 下标范围必须连续或显式指定
FORALL i IN 1..v_tab.COUNT 要求集合下标从 1 开始且连续。如果用 INDEX BY VARCHAR2 或中间 DELETE 过元素,会跳过空位导致漏更新。
- 稀疏集合必须用
INDICES OF v_tab:FORALL i IN INDICES OF v_tab UPDATE employees SET salary = v_tab(i).salary WHERE employee_id = v_tab(i).employee_id; - 若只更新某几个下标,可构造索引集合:
TYPE idx_tab IS TABLE OF PLS_INTEGER;,再用VALUES OF idx_tab -
SAVE EXCEPTIONS不是开关,是“延迟报错”机制:必须紧跟FORALL后用EXCEPTION WHEN OTHERS THEN包裹,并检查SQL%BULK_EXCEPTIONS.COUNT > 0
真实批量 UPDATE 的最小可靠模板
以下是一个防内存爆、可查错、适配大表的闭环结构:
DECLARE
TYPE t_emp IS TABLE OF employees%ROWTYPE;
v_emps t_emp;
CURSOR c_emp IS SELECT * FROM employees WHERE department_id = 10;
BEGIN
OPEN c_emp;
LOOP
FETCH c_emp BULK COLLECT INTO v_emps LIMIT 1000;
EXIT WHEN v_emps.COUNT = 0;
<pre class='brush:php;toolbar:false;'>FORALL i IN INDICES OF v_emps SAVE EXCEPTIONS
UPDATE employees
SET salary = v_emps(i).salary * 1.1
WHERE employee_id = v_emps(i).employee_id;
IF SQL%BULK_EXCEPTIONS.COUNT > 0 THEN
FOR j IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
DBMS_OUTPUT.PUT_LINE('Error at index ' || SQL%BULK_EXCEPTIONS(j).ERROR_INDEX ||
': ' || SQLERRM(-SQL%BULK_EXCEPTIONS(j).ERROR_CODE));
END LOOP;
END IF;END LOOP; CLOSE c_emp; END;
注意:分页大小不是越大越好,1000–5000 是较稳区间;真正上百万行时,还要考虑 undo 表空间压力,必要时加 COMMIT(但别在循环内每批都 COMMIT,会破坏事务一致性)。



















