<p>在 DELETE 触发器中安全获取删除行数据的唯一可靠方式是使用 :OLD.字段名,不可用 SELECT * FROM deleted 或查询原表,否则触发 ORA-04091 变异表错误;时间用 SYSDATE,客户端 IP 需中间层传入。</p>

如何在 DELETE 触发器里安全获取删除的行数据
直接用 :OLD.* 是唯一可靠方式,SELECT * FROM deleted 这种 SQL Server 写法在 Oracle 里根本不存在。Oracle 的行级触发器中,被删行的数据通过伪记录 :OLD 暴露,必须显式列出字段,比如 :OLD.id、:OLD.name。
常见错误是写成 INSERT INTO log_table SELECT * FROM ... —— Oracle 不允许在触发器里对原表做 DML 查询(尤其删除中查自己),会报 ORA-04091(“表正在变异”)。也不要用 SELECT COUNT(*) FROM t WHERE id = :OLD.id 去验证,既没必要又危险。
- 必须加
FOR EACH ROW,否则拿不到单行:OLD - 别在触发器里调用函数查原表或关联视图,极易触发变异表错误
- 如果表有 LOB、对象类型或嵌套表字段,
:OLD仍可访问,但注意赋值时需用DBMS_LOB.SUBSTR等处理
怎么记录操作时间与客户端 IP 地址
Oracle 没有内置函数直接返回客户端 IP,SYS_CONTEXT('USERENV', 'IP_ADDRESS') 在多数连接方式下返回空或代理地址,不可靠。真正可用的是 SYS_CONTEXT('USERENV', 'HOST')(主机名)和 SYS_CONTEXT('USERENV', 'OS_USER')(操作系统用户),但它们不等于网络来源。
若需真实 IP,必须依赖中间层(如应用服务器或连接池)传入,或改用 Oracle Net 的 SQLNET.ORA 配置启用 TRACE_LEVEL_CLIENT 并解析日志——触发器本身做不到。
- 时间一律用
SYSDATE,不是CURRENT_DATE(受会话时区影响) -
ORIGINAL_LOGIN()在 Oracle 里不存在,应改用SYS.LOGIN_USER或USER - 审计时间戳建议存为
DATE类型,避免用VARCHAR2存字符串,否则排序、范围查询全失效
为什么不能在 AFTER DELETE 触发器里再查原表
ORA-04091 错误就是这么来的。Oracle 在执行 DELETE 语句期间,会将目标表标记为“变异(mutating)”,此时任何对它的 SELECT、UPDATE 或 DELETE 都被禁止,哪怕只是检查是否存在。
有人想写 IF :OLD.status = 'active' THEN INSERT INTO audit_log ... 这没问题;但一旦改成 SELECT status FROM orders WHERE id = :OLD.id,立刻失败。这不是权限问题,是 Oracle 内核强制限制。
- 所有判断逻辑必须基于
:OLD字段完成,不要回查 - 如果真需要关联其他表(如客户信息),确保那张表不是触发器所在表,且已建好索引
- 避免在触发器里调用含 DML 的存储过程,同样可能触发变异表错误
日志表设计与权限容易被忽略的关键点
日志表必须对触发器所属用户(如 SCOTT)有 INSERT 权限,而且是直接授权,不能靠角色继承——触发器运行时角色被禁用。如果日志表在另一个 schema(如 AUDIT_SCHEMA.log_table),必须提前执行 GRANT INSERT ON AUDIT_SCHEMA.log_table TO SCOTT。
字段类型要预留余量:USER 最长 30 字节,ora_dict_obj_name 最长 128 字节,SYSDATE 对应 DATE,别用 TIMESTAMP 除非你明确需要毫秒精度。
- 别用
VARCHAR2(1)存操作类型('I'/'U'/'D'),万一以后加 'M'(merge)就扩容麻烦 - 日志表主键别设自增 ID,用
LOG_ID NUMBER GENERATED BY DEFAULT AS IDENTITY更稳妥(Oracle 12c+) - 如果审计要求高可用,考虑用异步通知(如 AQ)代替同步 INSERT,避免拖慢主事务


















