SAVE EXCEPTIONS 是 Oracle PL/SQL 中 FORALL 语句的子句,用于批量 DML 时跳过单条错误记录并继续执行其余操作,解决部分数据违规导致整批失败的问题。

SAVE EXCEPTIONS 是什么,它能解决什么问题
SAVE EXCEPTIONS 是 Oracle PL/SQL 中 FORALL 语句的一个子句,用于在批量 DML(如 INSERT、UPDATE、DELETE)中跳过单条出错的记录,继续执行后续语句。它不适用于普通循环或单条 SQL,也不影响事务整体提交——出错的那条不会被写入,但其余成功的仍会生效(除非显式回滚)。
常见场景是:用数组批量插入几百条数据,其中某几条因主键冲突、字段超长、非空约束失败而报错,你不想整批失败,而是想让其他行照常入库,并事后查出哪些失败了。
必须配合 FORALL 使用,不能单独写在普通循环里
SAVE EXCEPTIONS 只在 FORALL 中合法,下面这种写法是错的:
-- ❌ 错误:SAVE EXCEPTIONS 不能用于普通 LOOP FOR i IN 1..data_tab.COUNT LOOP INSERT INTO t VALUES (data_tab(i)); EXCEPTION WHEN OTHERS THEN NULL; -- 这不是 SAVE EXCEPTIONS,且无法捕获单条错误 END LOOP;
正确写法必须用 FORALL + BULK COLLECT + 异常处理结构:
DECLARE
TYPE id_tab IS TABLE OF t.id%TYPE;
ids id_tab := id_tab(1, 2, 3, 4);
errors SYS.ODCINUMBERLIST;
BEGIN
FORALL i IN INDICES OF ids SAVE EXCEPTIONS
INSERT INTO t(id) VALUES (ids(i));
<p>EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -24381 THEN -- 批量 DML 中部分失败的固定错误码
errors := SQL%BULK_EXCEPTIONS;
FOR j IN 1..errors.COUNT LOOP
DBMS_OUTPUT.PUT_LINE('第 ' || errors(j).ERROR_INDEX ||
' 行失败,错误码:' || errors(j).ERROR_CODE);
END LOOP;
ELSE
RAISE;
END IF;
END;</p>关键点:
-
FORALL i IN INDICES OF ids比1..ids.COUNT更安全(支持稀疏数组) - 必须检查
SQLCODE = -24381才能进入异常收集逻辑 -
SQL%BULK_EXCEPTIONS是一个索引从 1 开始的集合,每个元素含ERROR_INDEX(对应原数组下标)和ERROR_CODE(如 -1:主键冲突)
常见失败原因与对应 ERROR_CODE 查看方式
批量执行时,每条失败语句都会记录到 SQL%BULK_EXCEPTIONS,但错误码是 Oracle 内部编号,需查文档或用 SQLERRM 辅助判断:
-
-1:违反唯一约束(如主键、唯一索引) -
-1400:违反 NOT NULL 约束 -
-12899:值太大(如字符串超长) -
-2290:违反 CHECK 约束
你可以这样打印更可读的错误信息:
DBMS_OUTPUT.PUT_LINE('第 ' || errors(j).ERROR_INDEX ||
' 行失败:' || SQLERRM(-errors(j).ERROR_CODE));
注意:SQLERRM 接收的是负数错误码,所以要加负号。
事务控制和性能要注意的坑
FORALL ... SAVE EXCEPTIONS 不改变事务边界:所有成功的语句已写入 buffer,只要没 ROLLBACK,它们就对其他会话可见(取决于隔离级别)。这意味着:
- 如果你想“全部成功才提交”,就不能依赖
SAVE EXCEPTIONS,得先做预校验或改用临时表+MERGE - 没有异常时,
FORALL性能远高于逐条INSERT;但一旦触发异常,Oracle 需额外维护错误上下文,开销略增(通常可忽略) - 数组过大(如 > 10000 行)可能引发 PGA 内存压力,建议分批(比如每次 1000 行),并在外层循环中处理异常
容易被忽略的一点:SQL%BULK_EXCEPTIONS 只在 WHEN OTHERS 异常块中有效,且仅在 FORALL 抛出 -24381 时才被填充;如果 FORALL 前面的 PL/SQL 代码出错(比如数组越界),这个集合是空的。


















