FORALL比逐行INSERT快,因其将N次单条DML合并为一次批量操作,避免PL/SQL与SQL引擎反复切换;底层一次性传递绑定数组,省去重复解析执行。

为什么直接用FOR LOOP逐行INSERT会慢
因为每次循环都触发一次PL/SQL到SQL引擎的上下文切换,10万次INSERT就切换10万次。Oracle内部要反复解析、绑定、执行、返回,开销远超语句本身。尤其当目标表有索引、约束或触发器时,每行都要单独校验和维护。
BULK COLLECT + FORALL 的最小可行写法
必须同时用 BULK COLLECT 提取数据、FORALL 批量提交,缺一不可。只用 BULK COLLECT 而不用 FORALL,只是把数据读得快了,写还是逐行;只用 FORALL 但没 BULK COLLECT,集合里没数据也白搭。
- 声明两个类型一致的集合变量:一个存主键/ROWID(用于定位),一个存字段值(用于更新或插入)
-
FETCH ... BULK COLLECT INTO必须配合LIMIT N,否则大结果集可能撑爆PGA内存;常见值是 5000–10000 -
FORALL i IN 1 .. collection.count后跟INSERT,不能加WHERE或其他逻辑判断 - 每次
FORALL后手动COMMIT,避免事务过大导致UNDO空间不足或锁等待
DECLARE
TYPE t_id IS TABLE OF source_table.id%TYPE;
TYPE t_name IS TABLE OF source_table.name%TYPE;
l_ids t_id;
l_names t_name;
CURSOR c IS SELECT id, name FROM source_table WHERE status = 'A';
BEGIN
OPEN c;
LOOP
FETCH c BULK COLLECT INTO l_ids, l_names LIMIT 5000;
EXIT WHEN l_ids.COUNT = 0;
FORALL i IN 1 .. l_ids.COUNT
INSERT INTO target_table (id, name) VALUES (l_ids(i), l_names(i));
COMMIT;
END LOOP;
CLOSE c;
END;
容易被忽略的性能陷阱
很多人照着模板写了,速度却没提升,问题往往出在“看不见”的地方:
-
INSERT目标表如果有唯一索引,FORALL仍会逐条检查冲突——批量不等于跳过约束校验 - 没关掉目标表的索引,尤其是非关键字段上的索引,INSERT时每行都要更新索引块,拖慢整批速度
- 源查询没加
/*+ parallel(n) */或没走索引,BULK COLLECT等的是全表扫描,不是PL/SQL慢,是SQL慢 - 集合变量定义用了
INDEX BY BINARY_INTEGER,但实际用FORALL时推荐用嵌套表(TABLE OF ...),前者在12c+版本中某些场景有隐式转换开销
什么时候不该用BULK COLLECT + FORALL
它不是银弹。如果单次插入几百行,或者源数据本身带复杂业务逻辑(比如某字段要根据另一字段动态计算、要查关联表补值),硬套 BULK COLLECT 反而增加编码复杂度和调试成本。这种场景更适合用带子查询的单条 INSERT SELECT,或者干脆用外部ETL工具。
真正适合它的,是“结构清晰、可批量映射、无强依赖中间状态”的场景——比如从日志表归档到历史分区表、从接口临时表清洗进主业务表。这时候,LIMIT 值选多大、要不要关索引、是否启用并行,才值得花时间调优。


















