游标本身不是批量处理工具,它是逐行访问机制;真要批量处理,得靠bulk collect + forall,否则用显式游标循环fetch更新百万行,基本等于给数据库“喂慢动作毒药”。

直接说结论:游标本身不是批量处理工具,它是逐行访问机制;真要批量处理,得靠 BULK COLLECT + FORALL,否则用显式游标循环 FETCH 更新百万行,基本等于给数据库“喂慢动作毒药”。
为什么不能直接用显式游标循环做批量更新
显式游标本质是单行驱动——每次 FETCH 只取一行,配合 UPDATE 或 INSERT 就变成 N 次独立 SQL 执行。Oracle 要反复切换 PL/SQL 和 SQL 引擎上下文,I/O 和解析开销爆炸。
- 10 万行数据,用游标循环更新,可能耗时几分钟甚至更久;换成
FORALL,通常几秒内完成 - 事务日志(redo)写入量剧增,容易触发日志切换瓶颈
- 锁持有时间拉长,阻塞其他会话,尤其在
FOR UPDATE场景下极易引发锁等待 -
%ROWCOUNT、%FOUND这类属性只反映上一条语句,无法代表整个批次状态
真正高效的批量模式:BULK COLLECT + FORALL
这是 Oracle 官方推荐的批量路径,核心是把数据成批“搬进内存”,再成批“发回数据库”,避开逐行上下文切换。
-
BULK COLLECT INTO必须搭配集合类型(如TABLE OF ...%ROWTYPE),不能直接进普通变量 -
FORALL不是循环,它生成一条“伪批量语句”,由 Oracle 内部优化执行计划 - 必须用
INDICES OF或BETWEEN明确指定索引范围,否则空集合会报ORA-22160 - 示例中常见错误:
FORALL i IN 1..emp_table.COUNT—— 如果集合为空,COUNT是 0,导致下标越界
DECLARE
TYPE emp_tab IS TABLE OF employees%ROWTYPE;
emp_data emp_tab;
BEGIN
SELECT * BULK COLLECT INTO emp_data
FROM employees WHERE department_id = 50;
<p>FORALL i IN INDICES OF emp_data
UPDATE employees
SET salary = emp_data(i).salary * 1.1
WHERE employee_id = emp_data(i).employee_id;</p><p>COMMIT;
END;
/</p>
大表分批处理:LIMIT + 循环 + 手动事务控制
当数据量超过内存承受能力(比如千万级),硬塞 BULK COLLECT 会 OOM。这时要用 LIMIT 分片,但注意这不是“游标批量”,而是“带 LIMIT 的游标分页”。
- 每次
FETCH ... BULK COLLECT INTO ... LIMIT 10000,控制单次内存占用 - 每批结束后显式
COMMIT,避免 undo 表空间撑爆;但要考虑业务一致性,不是所有场景都适合中间提交 - 别依赖游标 %ROWCOUNT 判断是否取完——它返回本次 FETCH 行数,不是累计值
- 退出条件必须用
EXIT WHEN c_emp%NOTFOUND,且放在FETCH后立即判断,否则最后一轮会多执行一次
DECLARE
CURSOR c_emp IS SELECT employee_id, salary FROM employees;
TYPE t_emp IS TABLE OF c_emp%ROWTYPE;
emp_batch t_emp;
BEGIN
OPEN c_emp;
LOOP
FETCH c_emp BULK COLLECT INTO emp_batch LIMIT 10000;
EXIT WHEN emp_batch.COUNT = 0;
<pre class="brush:php;toolbar:false;">FORALL i IN 1..emp_batch.COUNT
UPDATE employees SET salary = emp_batch(i).salary * 1.1
WHERE employee_id = emp_batch(i).employee_id;
COMMIT; -- 按需决定是否提交END LOOP; CLOSE c_emp; END; /
最容易被忽略的三个细节
批量不等于安全:FORALL 出错默认整个批次失败,但你可以用 SAVE EXCEPTIONS + SQL%BULK_EXCEPTIONS 捕获具体哪几行出问题;BULK COLLECT 不做隐式类型转换,字段顺序、空值、精度必须和目标集合严格一致;生产环境千万别在没加 WHERE 条件的 SELECT * 上用 BULK COLLECT,查全表可能直接拖垮实例。











