应异步写日志(如redis队列+独立消费者),同步写则精简audit_log表结构、禁用非必要索引、避免触发器中查表或调用不支持函数;pg需防递归触发,sql server须正确处理inserted/deleted集合,oracle宜用after statement批量审计。

触发器里怎么安全写日志表,不卡主业务
直接写 INSERT INTO audit_log 很容易拖慢原事务,尤其在高并发更新场景下。根本原因是日志写入和业务更新绑在同一个事务里,锁表、磁盘 I/O、索引维护全得等它做完。
实操建议:
- 用异步方式剥离日志:MySQL 5.7+ 可考虑
INSERT DELAYED(仅 MyISAM 支持,已弃用),更稳妥的是把日志先写进内存队列(如 Redis List),再由独立消费者落库 - 如果必须同步写,确保
audit_log表是 InnoDB,且只有必要字段(id、table_name、operation、old_data、new_data、created_at),禁用所有非必需索引,created_at建普通索引即可 - 避免在触发器里调用函数或子查询查其他表——
SELECT会加读锁,可能引发死锁;也不要用UUID()或NOW()外部函数,部分 MySQL 版本不支持
PostgreSQL 中触发器审计如何避免 stack depth limit exceeded
递归触发是 PostgreSQL 审计最常踩的坑:比如在 UPDATE 触发器里又去 UPDATE audit_log,而 audit_log 上也有触发器,就可能无限嵌套。
实操建议:
- 用
pg_trigger_depth()在触发器函数开头做守卫:IF pg_trigger_depth() > 1 THEN RETURN NEW; END IF; - 不要给
audit_log表本身定义任何触发器——它只存日志,不参与业务逻辑 - 用
tg_op和tg_table_name区分操作类型,避免在BEFORE触发器里修改NEW字段后又触发同表另一个触发器
SQL Server 触发器审计时,inserted 和 deleted 表怎么取变更字段值
这两个临时表不是“一行一列”,而是集合,直接 SELECT * FROM inserted 在多行更新时会出错;另外,text、ntext、image 类型在新版本已废弃,但旧系统迁移时仍可能遇到无法读取问题。
实操建议:
- 永远用
JOIN或CROSS APPLY关联主表,而不是假设单行:SELECT i.id, i.name, d.name AS old_name FROM inserted i FULL OUTER JOIN deleted d ON i.id = d.id - 对大字段(如
varchar(max))做长度截断再存日志:LEFT(i.content, 2000),避免日志表膨胀过快 - 注意
deleted表在INSERT触发器中为空,inserted表在DELETE触发器中为空——没判空直接SELECT会返回空结果集,但不会报错,容易漏审计
Oracle 的 AFTER STATEMENT 触发器为什么比 AFTER EACH ROW 更适合批量审计
AFTER EACH ROW 每改一行就触发一次,1000 行更新 = 1000 次日志插入,IO 压力翻倍;而 AFTER STATEMENT 整个语句执行完才触发一次,配合 DBMS_LOGMNR 或 FLASHBACK_TRANSACTION_QUERY 才能拿到完整变更集。
实操建议:
- 优先用
AFTER STATEMENT+INSERT /*+ APPEND */批量写日志,减少 redo 日志压力 - 别依赖
:OLD和:NEW绑定变量——它们在 statement 级触发器里不可用;改用ORA_ROWSCN或SCN_TO_TIMESTAMP(ORA_ROWSCN)标记时间点 - 如果要记录具体字段变化,得自己在应用层或通过 LogMiner 解析 redo,触发器本身做不到细粒度字段级差异捕获
真正难的不是写触发器,而是想清楚“谁需要看这条日志”“保留多久”“查的时候要不要反向还原”。很多团队堆了半年审计日志,结果发现没人真去查,或者查一次要跑十分钟——这时候删表比修触发器更有效。










