Oracle触发器中需用:OLD.col和:NEW.col(带冒号前缀)访问字段值,仅限行级触发器;NULL比较须用IS NOT DISTINCT FROM或NVL处理,LOB等特殊类型需规避ORA-04091错误。

触发器里怎么拿到 OLD 和 NEW 的字段值
Oracle 触发器本身不提供像 PostgreSQL 那样的 OLD / NEW 记录变量,必须显式用 :OLD.column_name 和 :NEW.column_name 引用。这点容易被刚从其他数据库转来的开发者忽略,直接写 OLD.id 会报错 PLS-00201: identifier 'OLD.ID' must be declared。
关键约束:只能在 BEFORE UPDATE 或 AFTER UPDATE 行级触发器中使用 :OLD 和 :NEW;语句级触发器(FOR EACH STATEMENT)不支持。
- 更新前字段值必须写成
:OLD.col1,不能漏掉冒号和冒号后的点 - 如果字段允许 NULL,比较时要用
IS NOT DISTINCT FROM(Oracle 23c+)或手动处理 NULL 场景,否则:OLD.col1 != :NEW.col1在任一为 NULL 时恒为 FALSE - 对 LOB、对象类型、嵌套表字段,
:OLD/:NEW引用可能失效或报ORA-04091,这类字段建议跳过审计或改用物化视图日志
只记录真正变化的字段,避免冗余日志
逐个字段写 IF :OLD.col != :NEW.col THEN ... END IF; 很常见,但维护成本高、易遗漏。更稳妥的做法是拼接变化字段名与新旧值,统一写入 JSON 或键值对字符串。
例如审计表结构为 audit_log(table_name VARCHAR2(30), pk_value VARCHAR2(100), changed_fields CLOB, op_type CHAR(1)),可在触发器中构造:
DECLARE
v_changes CLOB := '';
BEGIN
IF NVL(:OLD.name, '###') != NVL(:NEW.name, '###') THEN
v_changes := v_changes || '"name":"' || :OLD.name || '→' || :NEW.name || '",';
END IF;
IF NVL(:OLD.status, '###') != NVL(:NEW.status, '###') THEN
v_changes := v_changes || '"status":"' || :OLD.status || '→' || :NEW.status || '",';
END IF;
-- 去掉末尾逗号再插入
IF LENGTH(v_changes) > 0 THEN
INSERT INTO audit_log VALUES ('users', :OLD.id, '{' || RTRIM(v_changes, ',') || '}', 'U');
END IF;
END;注意:NVL(..., '###') 是兼容 NULL 比较的惯用写法;若字段含单引号或换行,需额外 REPLACE 转义,否则 JSON 格式会崩。
触发器性能隐患:别在循环里查表或调用远程过程
审计日志写入本身是 DML 操作,在高并发更新场景下,如果触发器里还嵌套 SELECT 查询其他表(比如查用户名、部门名),极易引发锁等待甚至死锁。更糟的是调用 UTL_HTTP 或 DB Link 写远程库——这会让事务卡住数秒,拖垮整个业务链路。
- 审计表必须建在本地库,且字段尽量精简(主键值、表名、变化字段、时间戳、操作用户)
- 避免在触发器中执行
SELECT ... INTO,尤其不要查被更新表本身(会触发ORA-04091 mutating table) - 如需关联信息(如操作人姓名),应在应用层补全,或用异步队列(如 AQ)解耦,而不是塞进触发器
- 审计表建议关闭外键、不建索引(或仅对查询频次高的字段建函数索引),写入优先
ORA-04091 错误的典型诱因和绕过方式
当你在触发器里写 SELECT COUNT(*) FROM target_table WHERE id = :NEW.id,Oracle 就会报 ORA-04091: table XXX is mutating, trigger/function may not see it。这不是 bug,是 Oracle 为保证读一致性做的强制限制。
可行解法只有两个方向:
- 改用复合触发器(
CREATE OR REPLACE TRIGGER ... FOR EACH ROW COMPOUND),在AFTER EACH ROW收集变更,在AFTER STATEMENT统一处理(此时表已解锁) - 放弃在触发器内查原表,把需要的信息提前存到包变量或临时表(
GLOBAL TEMPORARY TABLE),但要注意事务边界和会话隔离 - 绝对不要用自治事务(
PRAGMA AUTONOMOUS_TRANSACTION)绕过 —— 它会让审计日志脱离主事务,主事务回滚时日志却已提交,造成数据不一致
真正难的不是写出能跑的日志逻辑,而是让审计不拖慢业务、不引入隐性一致性风险。多数线上事故,都出在把“能用”当“可用”上。


















