Oracle 游标、Bulk Collect 和 FORALL 有什么区别?如何优化批处理?
简化版
游标用于逐行处理查询结果,但逐行循环会产生大量 SQL 与 PL/SQL 上下文切换。BULK COLLECT 可以批量取数,FORALL 可以批量执行 DML,适合优化 PL/SQL 批处理。核心原则是能用集合 SQL 就不用游标,必须过程化处理时再用批量机制。
详细版
优化层次:
- 首选单条集合 SQL,如
insert into select、merge。 - 需要复杂过程逻辑时,用游标。
- 数据量大时,用
bulk collect limit分批取数。 - 批量 DML 用
forall,减少上下文切换。 - 加异常处理和提交策略,避免一次性占用过多内存。
逐行处理 100 万行可能上下文切换 100 万次;批量每次 1000 行,切换次数大幅下降。面试重点是集合化思维和批处理边界。
完整版教学
一、游标解决的是逐行遍历结果集
游标可以让 PL/SQL 按行读取查询结果,并在循环中执行逻辑。它适合每行处理逻辑复杂、无法用单条 SQL 表达的场景。
for r in (select id, amount from orders) loop
-- 逐行处理
null;
end loop;
但游标逐行处理容易慢,因为每一行都可能触发 SQL 引擎和 PL/SQL 引擎之间的上下文切换。
记忆钩子:游标能做逐行逻辑,但数据库最擅长的是集合,不是循环。
二、能用集合 SQL 就不要游标
如果逻辑可以用一条 SQL 完成,就不要写游标循环。比如把满足条件的订单状态批量更新:
update orders
set status = 'EXPIRED'
where status = 'PENDING'
and expired_at < sysdate;
这比游标查出每一行再逐条 update 更简单、更快、更容易让优化器选择好计划。
集合 SQL 通常是第一优先级,游标是确实需要过程化处理时的工具。
三、Bulk Collect 批量取数
BULK COLLECT 可以一次取多行到集合变量中,减少逐行 fetch 的开销。数据量大时要配合 LIMIT 分批,避免一次性占用过多 PGA。
fetch c bulk collect into l_ids limit 1000;
如果一次取 100 万行,PGA 可能暴涨;每次取 1000 或 5000 行,性能和内存更平衡。
100 万行逐行 fetch -> 100 万次取数
1000 行一批 -> 约 1000 次取数
四、FORALL 批量执行 DML
FORALL 用于批量执行 insert、update、delete,减少 PL/SQL 到 SQL 引擎的上下文切换。
forall i in 1 .. l_ids.count
update orders
set status = 'DONE'
where id = l_ids(i);
它不是普通 for 循环,而是批量绑定。对大量 DML 场景,FORALL 通常比逐条执行快很多。
五、批处理要设计提交策略
批量处理不能只追求一次做完。一次事务处理 1000 万行,Undo、Redo、锁持有时间都会很大,失败回滚也很痛苦。
常见策略是按批提交:
每批 1000 或 5000 行
记录处理进度
失败批次可重试
避免长事务占满 Undo
但提交太频繁也会增加开销。批大小要根据数据量、Undo 空间、业务一致性和恢复策略压测确定。
六、异常处理不能丢
批量 DML 中某些行可能失败。如果希望部分失败不影响全部,可以使用 SAVE EXCEPTIONS,之后检查异常数组。
forall i in 1 .. l_ids.count save exceptions
update orders set status = 'DONE' where id = l_ids(i);
这适合批量修复或导入场景。但如果业务要求全成功或全失败,就不应吞掉单行异常。
七、常见误区与追问
- 误区:游标是 Oracle 批处理的最佳方式。 能用集合 SQL 时,集合 SQL 通常更优。
- 误区:Bulk Collect 一次取越多越快。 取太多会占用大量 PGA,应该分批。
- 误区:FORALL 就是普通 for 循环。 FORALL 是批量绑定 DML,减少上下文切换。
- 追问:为什么逐行处理慢? SQL 引擎和 PL/SQL 引擎频繁切换,次数随行数放大。
- 追问:批量任务多久提交一次? 看 Undo、Redo、锁时间和恢复需求,通常分批提交并记录进度。
- 追问:部分行失败怎么办? 可用
SAVE EXCEPTIONS收集异常,再记录失败明细。
八、面试中可以这样落地
可以按优化路径回答:先尝试单条 SQL;不行再游标;数据量大时用 bulk collect limit 分批取数,用 forall 批量 DML,并设计批次提交和失败重试。
集合 SQL -> Bulk Collect 分批 -> FORALL 批量写 -> 批次提交 -> 失败记录
这个答案体现了 Oracle 批处理的核心经验:减少上下文切换,同时控制事务大小。
九、加强记忆
Oracle 批处理记住“集合优先,游标兜底,Bulk Collect 批量读,FORALL 批量写”。大批量任务还要关注 PGA、Undo、Redo、提交频率和异常恢复。面试时不要只讲语法,要讲为什么它能减少上下文切换。