sql server 2019 审计触发器核心是可靠捕获“改了什么、谁改的、改前改后值”,必须用after update配合inserted/deleted正确join,避免null比较陷阱,日志表需支持字段级差异,严禁触发器内update原表以防死循环。

SQL Server 2019 中创建数据审计触发器,核心不是写 CREATE TRIGGER 语句本身,而是确保它能可靠捕获“改了什么、谁改的、改前改后值分别是多少”——漏掉 inserted 和 deleted 的正确 JOIN,审计日志就只剩空壳。
必须用 AFTER UPDATE + 正确 JOIN inserted/deleted
UPDATE 审计触发器只能用 AFTER UPDATE,不能用 INSTEAD OF UPDATE。后者会拦截原操作,你得自己重写 UPDATE,极易丢数据或漏审计;而 AFTER UPDATE 保证原语句已成功执行,inserted 和 deleted 表里才有真实新旧值。
-
inserted是更新后的新行集合,deleted是更新前的旧行集合,二者结构与目标表完全一致 - 绝不能写
SELECT * FROM inserted单独查——多行时结果不可控,必须和主表或日志表JOIN - 典型写法是
INSERT INTO audit_log (...) SELECT i.*, d.*, SYSTEM_USER, GETDATE() FROM inserted i INNER JOIN deleted d ON i.id = d.id - 如果目标表主键是复合键,JOIN 条件要完整覆盖,比如
ON i.order_id = d.order_id AND i.line_no = d.line_no
判断字段是否被修改,别用 != 比较 NULL
直接写 i.Price != d.Price 在任一值为 NULL 时结果恒为 UNKNOWN,导致条件失效——这是审计漏记录最隐蔽的坑。
- 必须用内置函数
UPDATE(Price)判断 Price 字段是否参与了本次 UPDATE 语句(哪怕设成 NULL 或原值) -
UPDATE()不关心值变没变,只关心 SQL 里有没有出现该列,适合“只要动了这列就审计”的场景 - 若需精确判断值是否真变了(比如跳过未实际变更的 UPDATE),得配合 ISNULL 或 COALESCE 做安全比较:
ISNULL(i.Price, -1) != ISNULL(d.Price, -1)
审计日志表设计要支持多行 & 字段级差异
一张只存 table_name、action_time、user 的日志表,根本撑不起真实审计需求——你没法回溯“Price 从 199.00 改成了 249.00”。
- 日志表字段建议包含:原始表所有列(加
_old/_new后缀)、操作类型(UPDATE)、修改字段列表(modified_columnsvarchar(max))、操作人(SYSTEM_USER或ORIGINAL_LOGIN())、时间(GETDATE()) - 不要把
inserted和deleted直接SELECT *插入日志表——列顺序/数量必须严格匹配,否则报错 - 批量更新时,
inserted和deleted都是多行结果集,日志插入必须用集合操作,禁止DECLARE @id INT; SELECT @id = id FROM inserted这类标量赋值
AFTER 触发器里别 UPDATE 原表,小心死循环
在 AFTER UPDATE 触发器中再对同一张表执行 UPDATE,SQL Server 默认允许,但极大概率引发无限递归——每次 UPDATE 又触发自身,直到达到嵌套层级上限(默认 32)然后报错 Maximum stored procedure nesting level exceeded。
- 审计场景下,只允许
INSERT到日志表、UPDATE其他关联表(如库存表)、RAISERROR或THROW抛异常 - 真需要改原表数据(比如自动补全
updated_by),改用INSTEAD OF UPDATE,但必须手动重放整个 UPDATE 逻辑,成本高、风险大 - 可通过
TRIGGER_NESTLEVEL()检查当前嵌套深度,但属于兜底手段,不该作为常规设计
真正难的不是语法,是每一条 INSERT INTO audit_log 语句背后,都要想清楚:JOIN 条件能否覆盖所有主键、NULL 怎么安全比、多行会不会静默截断、触发器是否意外修改了不该碰的表——这些细节不抠,日志看着有,查起来全是空或错。











