结论是用BULK COLLECT + FORALL分批处理,单次FETCH控制在5000–10000行并显式COMMIT最稳妥;普通FOR循环逐行处理百万数据会导致百万次上下文切换、PGA溢出、ORA-01555等严重问题。

直接上结论:用 BULK COLLECT + FORALL 分批处理,单次 FETCH 量控制在 5000–10000 行之间,配合显式 COMMIT,是百万级数据循环最稳的路径。硬套单次全量加载或裸写 FOR 循环,基本等于卡死或 OOM。
为什么不能用普通 FOR 循环逐行处理百万数据
每执行一次 INSERT 或 UPDATE,PL/SQL 和 SQL 引擎就要切换一次上下文,百万次就是百万次切换 —— 这不是慢,是资源耗尽。实测中,10 万行用普通循环插入耗时 6.39 秒;同样数据量改用 FORALL 后压到 0.72 秒;但若把 100 万行一次性塞进 FORALL,反而涨到 274 秒,原因就是内存溢出、PGA 压力过大、undo 表空间撑爆。
- 普通循环本质是“N 次单条 SQL 执行”,网络往返、解析、执行开销叠加
- Oracle 默认游标逐行 fetch,不加
LIMIT就可能把整张千万级表拉进 PGA - 没
COMMIT的长事务会锁表、阻塞其他会话、触发 ORA-01555 快照过旧
怎么写一个安全的 BULK COLLECT + FORALL 分批块
核心是“分段取、批量绑、及时交”。下面这段代码能直接复用,只需替换表名、字段和条件:
DECLARE
TYPE t_id_tab IS TABLE OF source_table.id%TYPE;
l_ids t_id_tab;
CURSOR c_data IS
SELECT id FROM source_table WHERE status = 'NEW';
BEGIN
OPEN c_data;
LOOP
FETCH c_data BULK COLLECT INTO l_ids LIMIT 10000;
EXIT WHEN l_ids.COUNT = 0;
<pre class="brush:php;toolbar:false;">FORALL i IN 1..l_ids.COUNT
INSERT INTO target_table (id, name, created_at)
SELECT id, name, SYSDATE
FROM source_table
WHERE id = l_ids(i);
COMMIT; -- 每批提交,释放 undo 和锁END LOOP; CLOSE c_data; END;
-
LIMIT 10000是经验值:太小(如 100)导致循环次数多、调度开销上升;太大(如 50000)易触发 ORA-04030(内存不足) -
FORALL只能绑定简单类型数组(NUMBER、VARCHAR2等),不能直接用%ROWTYPE集合做VALUES v_table(idx),否则报错 - 必须显式
COMMIT,且放在FORALL之后、END LOOP之前;不要依赖自治事务或外部 commit
常见报错和绕过方式
实际跑的时候,大概率遇到这几个错误,不是语法问题,而是机制踩坑:
-
ORA-06502: PL/SQL: numeric or value error: Bulk Bind: Truncated Bind:某列实际长度超了集合定义长度,比如VARCHAR2(10)存了 12 字符。解决:用%TYPE定义(如source_table.name%TYPE)代替硬编码长度 -
ORA-04030: out of process memory:LIMIT设太高,或集合变量未及时释放。解决:缩小LIMIT,并在每次循环末尾加l_ids.DELETE; -
ORA-01403: no data found出现在SELECT ... BULK COLLECT INTO:说明查询结果为空,但集合未初始化。解决:声明时加:= t_id_tab();,或用IF l_ids.COUNT > 0 THEN ... END IF;包裹 DML - 目标表有唯一约束冲突?
FORALL不支持SAVE EXCEPTIONS的写法(那是INSERT ... SELECT场景),得先用MERGE或前置去重逻辑
真正麻烦的从来不是语法,而是批量过程中 undo 表空间增长、归档日志暴增、以及目标表索引维护带来的隐性延迟。如果表上有多个非空索引,考虑临时禁用再重建,比边插边维护快一个数量级 —— 这个细节,90% 的人上线前根本没想到。











