必须显式加 SAVE EXCEPTIONS 才能捕获批量DML错误;ORA-24381是正常机制信号,表示错误已存入SQL%BULK_EXCEPTIONS;ERROR_INDEX为执行序号,需按遍历方式映射原下标;取错因须用SQLERRM(-ERROR_CODE);批量大小应据内存与表结构调优;事务仍需手动控制提交或回滚。

必须显式加 SAVE EXCEPTIONS,否则任意一条失败整批回滚,且不暴露具体哪条出错。
ORA-24381 是 SAVE EXCEPTIONS 的“成功信号”
只要用了 FORALL ... SAVE EXCEPTIONS 且其中某条 DML 报错(比如主键冲突、字段超长、约束违反),Oracle 就会抛 ORA-24381: array DML error ——这不是故障,是机制触发的正常异常。它告诉你:“有错,但我已存进 SQL%BULK_EXCEPTIONS,别慌。”
常见误判:看到 ORA-24381 就去查网络或权限,其实只需在 EXCEPTION 块里处理 SQL%BULK_EXCEPTIONS 即可。
- 不加
SAVE EXCEPTIONS时,错在哪条根本不知道,堆栈只显示ORA-06512到 FORALL 行号 - 加了但没捕获
ORA-24381,异常直接上抛,程序中断 - 捕获后不检查
SQL%BULK_EXCEPTIONS.COUNT,等于白加
ERROR_INDEX 指的是执行序号,不是原数组下标
SQL%BULK_EXCEPTIONS(i).ERROR_INDEX 返回的是 FORALL 实际执行的第几条语句(从 1 开始),和你的源集合下标可能完全错位。直接用它去取 id_arr(SQL%BULK_EXCEPTIONS(i).ERROR_INDEX) 在多数情况下是错的。
正确做法取决于你如何遍历:
- 若用
FORALL i IN id_arr.FIRST .. id_arr.LAST,且id_arr是稠密数组(无空洞),则ERROR_INDEX可直接对应id_arr下标 - 若用了
INDICES OF idx_list,ERROR_INDEX指向的是idx_list的位置,得再映射一层:orig_idx := idx_list(SQL%BULK_EXCEPTIONS(i).ERROR_INDEX); failed_id := id_arr(orig_idx); - 若用了
VALUES OF val_list,逻辑更复杂,建议避免,改用稠密数组 + 显式过滤
取错误信息必须用 SQLERRM(-ERROR_CODE)
在异常处理块里直接调用 SQLERRM,返回的永远是最后一条语句的错误信息,不是当前这条。要拿到每条失败记录的真实报错,必须用:
DBMS_OUTPUT.PUT_LINE( 'Failed at position ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX || ', code ' || SQL%BULK_EXCEPTIONS(i).ERROR_CODE || ', message: ' || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE) );
注意负号:-SQL%BULK_EXCEPTIONS(i).ERROR_CODE 才是 SQLERRM 能识别的格式;漏掉负号会返回“未定义错误”之类模糊信息。
批量大小与内存控制不能只看 COUNT
很多人设 LIMIT 10000 然后 FORALL i IN 1..arr.COUNT,结果 PGA 暴涨、GC 频繁,性能反而下降。关键不在“总条数”,而在单次绑定的数据体积和目标表结构:
- 目标表有 5 个索引 + 2 个触发器?FORALL 的优势会快速衰减,建议 LIMIT 200~500
- 字段含
CLOB或长VARCHAR2?实际内存占用远超行数,LIMIT 得砍半 - 用
BULK COLLECT INTO时,务必配合LIMIT,别让集合无限增长 - 事务边界要自己控:FORALL 不自动提交,别忘了在循环外或每批后
COMMIT
最易被忽略的一点:SAVE EXCEPTIONS 不改变事务原子性 —— 成功的那几条已写入,失败的被跳过,整个 FORALL 所在事务仍需你决定是 COMMIT 还是 ROLLBACK。别默认以为“有 SAVE EXCEPTIONS 就安全了”。


















