唯一解法是compound trigger:行级块仅收集:new/:old至局部变量,语句级块(after statement)再查本表;禁用包变量、自治事务及临时表,否则破坏一致性或并发安全。

在 Oracle 11g 中,COMPOUND TRIGGER 是唯一能真正避开 ORA-04091 的机制——它不绕开规则,而是把查表动作挪到 Oracle 允许的时间点。
为什么普通行级触发器查本表必报 ORA-04091
Oracle 不是“不让写 SQL”,而是禁止在 BEFORE EACH ROW 或 AFTER EACH ROW 阶段访问正被 DML 修改的同一张表。哪怕只写 SELECT COUNT(*) FROM orders WHERE id = :NEW.id,也会触发检查。
- 这不是语法错误,是读一致性机制在起作用:事务未提交,表处于“中间态”,查询结果不可定义
- 游标、函数封装、子查询都不例外——只要底层执行了对触发表的
SELECT,就报错 -
PRAGMA AUTONOMOUS_TRANSACTION能让代码跑起来,但查不到当前事务未提交的数据,且破坏原子性
COMPOUND TRIGGER 必须分清四个阶段的职责
复合触发器不是加个 COMPOUND 关键字就完事,关键在于逻辑必须严格按阶段隔离:
-
BEFORE EACH ROW和AFTER EACH ROW:只做轻量操作——收集:NEW/:OLD值、赋值给局部变量、简单校验 -
BEFORE STATEMENT:可安全查原表(如预读聚合值),但不能依赖行级数据 -
AFTER STATEMENT:唯一允许查原表 + JOIN + 更新其他表的位置;所有最终处理必须放这里
漏掉任一环节,比如在 AFTER EACH ROW 里写 SELECT,照样报 ORA-04091。
正确声明和使用 PL/SQL 局部变量暂存数据
行级阶段收集的数据,必须存在触发器内部声明的 PL/SQL 变量里,绝不能用包变量或全局临时表:
- 声明方式示例:
TYPE t_ids IS TABLE OF orders.order_id%TYPE INDEX BY PLS_INTEGER; - 变量必须在触发器声明区定义:
l_ids t_ids;,不能放在包里 - 在
AFTER EACH ROW中只做:l_ids(l_ids.COUNT + 1) := :NEW.order_id; - 在
AFTER STATEMENT中统一处理:FOR i IN 1..l_ids.COUNT LOOP ... SELECT ... FROM orders ... WHERE id = l_ids(i) ... END LOOP;
用包变量会导致并发事务互相覆盖;用 GLOBAL TEMPORARY TABLE 要额外建表、INSERT、清理,且无索引时性能差,还可能混入递归触发的无关行。
最容易被忽略的细节:AFTER STATEMENT 里的 SQL 必须显式处理空集合
AFTER STATEMENT 块不会自动跳过空集合,如果 l_ids.COUNT = 0,但代码里写了 FOR i IN 1..l_ids.COUNT,循环体不会执行——这没问题;但如果用了 FORALL i IN INDICES OF l_ids,而 l_ids 是空的,Oracle 11g 会直接报错 ORA-22160: element at index [1] does not exist。
所以实际写法要加保护:
IF l_ids.COUNT > 0 THEN
FORALL i IN INDICES OF l_ids
UPDATE orders SET audit_flag = 'Y' WHERE order_id = l_ids(i);
END IF;
这个检查在 Oracle 11g 中不能省——它不像 12c+ 那样对空集合更宽容。











