Oracle原生不支持开箱即用的行级审计,需用自治事务触发器实现;日志表须独立表空间、存JSON化新旧值、用SYSTIMESTAMP和SYS_CONTEXT捕获上下文,且触发器不可影响主事务。

Oracle 原生不支持开箱即用的“行级审计”(如 MySQL 的 binlog 那种逐行变更记录),AUDIT 命令只到语句级或对象级,DBMS_FGA 虽能捕获 DML 条件匹配的行访问,但不记录旧值/新值。真要记录谁、什么时候、改了哪几行的具体数据,必须靠 CREATE TRIGGER 自定义实现 —— 但直接在业务表上建触发器风险高,容易拖慢性能、引发锁争用,甚至导致事务失败。
为什么不能直接在业务表上写 AFTER UPDATE 触发器
常见错误是:用户在 EMPLOYEES 表上直接建 AFTER UPDATE 触发器,试图把 :OLD 和 :NEW 插入日志表。这会带来三个硬伤:
- 触发器执行期间持有行级锁,UPDATE 未提交前,其他会话无法读取该行(即使加
SELECT ... FOR UPDATE NOWAIT也会报ORA-00054) - 如果日志表和业务表在同一个表空间,且日志表插入失败(比如空间不足、约束冲突),整个业务 UPDATE 就会回滚,破坏应用逻辑
-
USER和SYSDATE在触发器里看似可靠,但实际可能不准:比如用 DB Link 远程调用、或通过中间件连接池复用 session,USER是远程端用户,SYSDATE可能被 NTP 漂移影响
安全可行的日志表结构与触发器写法
真正可用的方案是:日志表独立表空间 + 触发器仅做最小化写入 + 显式捕获上下文。关键点如下:
- 日志表必须指定
TABLESPACE,且不能是SYSTEM;推荐用单独小文件表空间,比如LOG_TBS,避免挤占核心对象空间 - 字段至少包含:
LOG_ID(序列)、TARGET_TABLE(varchar2(30))、OPERATION('INSERT'/'UPDATE'/'DELETE')、PK_VALUE(主键值,varchar2 方便兼容不同类型)、OLD_ROW和NEW_ROW(clob,存 JSON 化的旧/新行)、OS_USER(用SYS_CONTEXT('USERENV', 'OS_USER'))、IP_ADDRESS(SYS_CONTEXT('USERENV', 'IP_ADDRESS'))、LOG_TIME(SYSTIMESTAMP,比SYSDATE更精确) - 触发器体必须用
PRAGMA AUTONOMOUS_TRANSACTION,否则日志写入和业务事务绑定,一挂全挂 - 不要在触发器里做复杂逻辑(如查关联表、调存储过程),只做 INSERT + COMMIT
示例片段(以 EMPLOYEES 为例):
使用 Infinimo AI 设计制作手持产品短视频:Avatar 首帧合成、AI 脚本、图转视频提交与轮询。适用于可购物短视频、Avatar 主导的产品演示和商品 listing 宣传视频。
CREATE OR REPLACE TRIGGER emp_audit_trg
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
v_json_old CLOB;
v_json_new CLOB;
BEGIN
IF INSERTING THEN
INSERT INTO audit_log (target_table, operation, pk_value, new_row, os_user, ip_address, log_time)
VALUES ('EMPLOYEES', 'INSERT', :NEW.employee_id,
JSON_OBJECT('employee_id': :NEW.employee_id, 'name': :NEW.first_name || ' ' || :NEW.last_name),
SYS_CONTEXT('USERENV', 'OS_USER'), SYS_CONTEXT('USERENV', 'IP_ADDRESS'), SYSTIMESTAMP);
ELSIF UPDATING THEN
INSERT INTO audit_log (target_table, operation, pk_value, old_row, new_row, os_user, ip_address, log_time)
VALUES ('EMPLOYEES', 'UPDATE', :OLD.employee_id,
JSON_OBJECT('employee_id': :OLD.employee_id, 'salary': :OLD.salary),
JSON_OBJECT('employee_id': :NEW.employee_id, 'salary': :NEW.salary),
SYS_CONTEXT('USERENV', 'OS_USER'), SYS_CONTEXT('USERENV', 'IP_ADDRESS'), SYSTIMESTAMP);
END IF;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
NULL; -- 不抛错,避免影响主事务
END;audit_trail=NONE 时,触发器日志是否还有效
完全有效,而且更可控。因为:
-
AUDIT_TRAIL=NONE只禁用 Oracle 内置审计(DBA_AUDIT_TRAIL等视图为空),不影响 PL/SQL 触发器执行 - 自定义触发器日志独立于
AUD$表,不占用SYSTEM表空间,也不受AUDIT_SYS_OPERATIONS参数影响 - 但要注意:如果数据库未运行
cataudit.sql,DBA_AUDIT_TRAIL视图本身可能不存在,但这和你的触发器日志表毫无关系
真正要检查的是:触发器是否启用(SELECT status FROM user_triggers WHERE trigger_name = 'EMP_AUDIT_TRG')、日志表所在表空间是否 online、是否有 insert 权限。
细粒度审计(FGA)和触发器方案怎么选
看你要解决的问题本质:
- 要查“谁在什么时间访问过某张表的哪些行”,且只关心访问行为(不关心改没改、改成啥),用
DBMS_FGA.ADD_POLICY。它不写日志表,只往DBA_FGA_AUDIT_TRAIL插记录,开销比触发器低得多 - 要还原“某条记录的历史变更链”,比如查工资从 8000→12000→15000 的完整过程,必须用触发器 + 存 OLD/NEW 值。FGA 不提供旧值/新值
- 如果审计策略频繁变动(比如今天审 salary,明天审 department_id),触发器要反复编译,而 FGA 可用
DBMS_FGA.ENABLE_POLICY/DISABLE_POLICY动态开关
生产环境常见组合:FGA 审计敏感列访问 + 触发器审计关键表全量变更。两者不冲突,但日志表设计必须隔离,别混在一起。

















