必须使用AFTER触发器而非INSTEAD OF,因其确保日志记录在数据已提交且INSERTED/DELETED表内容稳定后执行,避免逻辑遗漏、脏读、死锁;需显式比对字段级变更并用JSON存储,日志表须独立设计、禁用递归触发。

SQL Server 的触发器能自动记录变更,但必须用 AFTER 触发器配合 INSERTED 和 DELETED 临时表,不能依赖 INSTEAD OF 或直接读取原表——否则可能漏记录、死锁或读到未提交数据。
为什么必须用 AFTER INSERT/UPDATE/DELETE 而不是 INSTEAD OF
INSTEAD OF 触发器会替代原操作执行,你得手动重写插入/更新/删除逻辑,稍有遗漏就会丢失业务行为;而日志的核心是“真实发生过的变更”,AFTER 确保日志写在数据已落盘之后,且 INSERTED 和 DELETED 表内容稳定、无并发干扰。
-
INSERTED在INSERT或UPDATE中有值,DELETED在UPDATE或DELETE中有值 - 不要在触发器里查原表(比如
SELECT * FROM Orders),可能读到脏数据或引发锁等待 - 避免在触发器中调用远程服务器、发邮件、写文件等耗时操作——会拖慢主事务
如何安全捕获字段级变更(尤其 UPDATE)
只记录整行太粗粒度,用户常问“谁改了哪个字段”,得比对 INSERTED 和 DELETED。但注意:NULL 值比较要用 IS NULL,不能用 = NULL;字符串比较要考虑 ANSI_NULLS OFF 风险(建议始终开启)。
- 用
CASE WHEN i.Status d.Status OR (i.Status IS NULL AND d.Status IS NOT NULL) OR (i.Status IS NOT NULL AND d.Status IS NULL)判断字段是否真变了 - 把变更字段拼成 JSON 或键值对字符串更易解析,例如:
'Status:' + ISNULL(i.Status, 'NULL') + '→' + ISNULL(d.Status, 'NULL') - 别用
SELECT *把所有列都记一遍——性能差,且加列后触发器会崩;显式列出要审计的字段
日志表设计和写入时的关键约束
日志表必须独立于业务表,且字段要预留扩展性。常见坑是没设 NOT NULL 或长度不足,导致触发器因截断失败而回滚整个事务。
- 必加字段:
LogID(IDENTITY)、TableName(sysname)、Operation(CHAR(1),'I'/'U'/'D')、RowID(对应业务表主键,类型一致)、ChangedBy(从ORIGINAL_LOGIN()或上下文获取)、ChangeTime(GETDATE()) - 变更详情字段建议用
NVARCHAR(MAX)存 JSON,别拆成固定列——否则改业务表结构就得同步改日志表和所有触发器 - 避免在日志表上建过多索引,尤其是含
ChangedBy或ChangeTime的组合索引——写入放大严重;按需建(TableName, ChangeTime)范围查询索引即可
最易被忽略的是触发器嵌套和递归:如果日志表本身也挂了触发器,或者业务表有级联外键,AFTER 触发器可能意外触发多次。务必在触发器开头加 IF NOT EXISTS (SELECT * FROM sys.dm_exec_requests WHERE session_id = @@SPID AND status = 'running' AND command = 'EXECUTE') 类似防护不现实——正确做法是关掉 RECURSIVE_TRIGGERS 数据库选项,并确保日志表无触发器。

















