FORALL不支持MERGE,须拆为INSERT+UPDATE并配合SELECT FOR UPDATE、批量500~2000、禁用AUTOCOMMIT及正确处理SAVE EXCEPTIONS索引错位。

FORALL 本身不支持 MERGE,必须拆成 INSERT + UPDATE 两条语句分别用 FORALL 执行;高并发下还要加 SELECT FOR UPDATE 预占行、控制批量大小在 500~2000、禁用 AUTOCOMMIT,并手动处理 SAVE EXCEPTIONS 的索引错位问题。
FORALL 不能直接写 MERGE 语句
Oracle 的 FORALL 只接受静态的 INSERT、UPDATE、DELETE,不支持 MERGE。试图写 FORALL i IN 1..arr.COUNT MERGE INTO t USING ... 会立刻报 ORA-06550。
实操只能分两步走:
- 先用
FORALL批量INSERT(带WHERE NOT EXISTS子查询或用INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX */配合唯一约束) - 再用
FORALL批量UPDATE(WHERE id IN 绑定集合) - 或者反向:先
UPDATE,再对SQL%ROWCOUNT = 0的部分补INSERT,但需额外查漏
高并发下必须预占行,否则丢失更新
多个会话同时跑同一批 ID 的 FORALL UPDATE,若没加锁,后提交者会覆盖前者的修改——这不是 FORALL 的问题,而是业务逻辑缺锁。
正确做法是在 FETCH BULK COLLECT 前,用显式游标加 SELECT ... FOR UPDATE SKIP LOCKED:
DECLARE
CURSOR c_data IS
SELECT id, name, status FROM source_tab
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED;
TYPE t_id_tab IS TABLE OF source_tab.id%TYPE;
l_ids t_id_tab;
BEGIN
OPEN c_data;
LOOP
FETCH c_data BULK COLLECT INTO l_ids LIMIT 500;
EXIT WHEN l_ids.COUNT = 0;
FORALL i IN 1..l_ids.COUNT
UPDATE target_tab SET name = ..., status = 'done'
WHERE id = l_ids(i);
COMMIT;
END LOOP;
CLOSE c_data;
END;
-
SKIP LOCKED避免会话阻塞,让不同会话能并行处理不同子集 - 游标里必须有
ORDER BY,否则FOR UPDATE行为不可预测 - 别用隐式游标
FOR rec IN (SELECT ...),它不支持FOR UPDATE
SAVE EXCEPTIONS 的 ERROR_INDEX 容易取错数据
启用 SAVE EXCEPTIONS 后,SQL%BULK_EXCEPTIONS(i).ERROR_INDEX 返回的是当前 FORALL 执行批次内的序号(从 1 开始),不是你原始集合的下标。
比如你传入的 id_arr 有 500 个元素,第 3 条和第 498 条失败,ERROR_INDEX 就是 3 和 498 —— 这没问题;但如果你用了 INDICES OF idx_list 跳过空位,而 idx_list 是 (1,5,10,15,...),那 ERROR_INDEX = 2 对应的是 idx_list(2)=5,得再查一层才能拿到真实 ID。
- 没用
INDICES OF:直接failed_id := id_arr(SQL%BULK_EXCEPTIONS(i).ERROR_INDEX) - 用了
INDICES OF idx_list:必须orig_idx := idx_list(SQL%BULK_EXCEPTIONS(i).ERROR_INDEX); failed_id := id_arr(orig_idx) - 错误码要写
SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE),不能只调SQLERRM
批量大小和事务控制是性能关键
设 LIMIT 10000 或盲目用 FORALL i IN 1..arr.COUNT,在高并发下反而更慢:PGA 内存争抢、临时段写入、锁等待都会放大。
- OLTP 场景下,
BULK COLLECT LIMIT和FORALL批次统一用500~2000,这是 19c 实测的甜点区 - 务必关闭
AUTOCOMMIT,否则每次FORALL结束自动提交,退化成单条执行 - 每批后显式
COMMIT,但别每条都COMMIT——1000 行一次比 100 行一次吞吐高 30%+ - 目标表若有 3 个以上索引或触发器,建议把批量再压到
500,避免日志和 latch 竞争激增
真正卡住的往往不是语法,而是集合 COUNT 不一致、ERROR_INDEX 直接当原下标用、或者忘了 FOR UPDATE SKIP LOCKED —— 这些地方一错,高并发下要么报错,要么静默丢数据。











