必须使用AFTER触发器,确保原始操作完成后再记录;inserted/deleted表结构固定需正确JOIN;用SYSUTCDATETIME()和DATETIME2(3)保障时间精度;日志表须建聚集索引并分区,禁用外键和嵌套触发器。

能用,但得避开几个关键坑——尤其是 AFTER 触发器对多行操作的处理、inserted/deleted 表的空值陷阱,以及时间戳精度和事务一致性问题。
触发器必须用 AFTER,不能用 INSTEAD OF
记录历史变更必须在原始 DML 执行完成之后发生,否则会丢失真实数据状态。比如 UPDATE 操作中,INSTEAD OF 会拦截原语句,你看到的 inserted 和 deleted 是你手动构造的,不是数据库实际写入前后的快照。
AFTER 才能确保:原始行已落盘、约束已校验、外键已检查、事务上下文仍有效。
- 只对目标表建
AFTER INSERT, UPDATE, DELETE——不要拆成三个独立触发器,否则无法统一处理复合操作(如一次UPDATE影响 1000 行) - 避免在触发器里再做
INSERT/UPDATE/DELETE目标表本身,否则可能引发递归或死锁 - 如果日志表和主表不在同一数据库,需显式加四部分命名(如
OtherDB.dbo.ChangeLog),否则跨库引用失败
inserted 和 deleted 表不是“有数据才存在”,而是“结构固定、内容按操作填充”
常见错误是写 IF EXISTS(SELECT * FROM inserted) 后直接 SELECT * FROM inserted,却没意识到:当触发器因批量操作被调用时,inserted 可能含上百行,而你的 INSERT INTO log 语句若没写 JOIN 或 FROM inserted,就会只取一行或报错。
更危险的是 UPDATE 场景:你必须同时读 inserted(新值)和 deleted(旧值),且二者通过主键关联。如果主键不是唯一或缺失,JOIN 会出错或漏数据。
-
INSERT:只读inserted,deleted为空(但表结构存在,不可SELECT * FROM deleted) -
DELETE:只读deleted,inserted为空 -
UPDATE:必须JOIN inserted i ON i.ID = d.ID关联两表,不能假设顺序或行数一致 - 永远用
SET NOCOUNT ON开头,否则客户端可能因触发器返回额外结果集而中断
日志表字段设计要匹配主表 + 元信息,且禁用 GETDATE() 直接赋值
用 GETDATE() 在触发器里取时间,看似简单,实则埋雷:它返回的是语句开始时间,不是事务提交时间;高并发下多个触发器可能拿到毫秒级相同时间戳,导致排序混乱。
正确做法是用 SYSUTCDATETIME()(更高精度、UTC 时区),或更稳妥地——让日志表字段默认为 DEFAULT SYSUTCDATETIME(),触发器插入时显式省略该列。
- 日志表至少包含:所有主表字段(保持结构镜像)、
ChangeType VARCHAR(10)(值为'INSERT'/'UPDATE'/'DELETE')、ChangeTime DATETIME2(3)(推荐DATETIME2(3),比DATETIME精确到毫秒且无闰秒问题) - 不要在日志表里存
SYSTEM_USER或ORIGINAL_LOGIN()作为操作人——应用层连接池复用会导致身份失真;如需审计,应由应用传入@AppUserID参数并透传 - 主表若有计算列、
IDENTITY或ROWVERSION,日志表对应字段应设为NULLABLE并留空,否则插入失败
性能瓶颈不在触发器逻辑,而在日志表的索引与分区
一个没索引的日志表,跑满半年后单次 INSERT 可能从 2ms 慢到 800ms——因为每次写都要扫描全表找插入位置(堆表)或维护无用的非聚集索引。
真正影响吞吐的是日志表本身的物理设计,不是触发器 SQL 写得多精妙。
- 必建聚集索引:按
ChangeTime DESC(倒序),这样新日志总追加在末尾,避免页分裂 - 如果按业务主键查历史(如查某用户所有变更),再加一个非聚集索引:(
BusinessKey) INCLUDE (ChangeType,ChangeTime) - 日志量大(日增 > 50 万行)时,必须按月/季度分区,用
ChangeTime做分区函数,否则DELETE过期数据会锁表数分钟 - 禁止在日志表上建触发器、外键或启用 CDC——这会让写入链路变成“主表 → 触发器 → 日志表 → 日志表触发器”,彻底失控
最易被忽略的一点:触发器内不能调用 WAITFOR、不能执行远程查询、不能显式开启事务(BEGIN TRAN)。它天生运行在父语句的事务中,回滚会连带清空日志表插入——所以日志表必须允许脏读(READ UNCOMMITTED)或接受“日志写入失败导致主操作失败”的权衡。

















