能做,但必须用AFTER触发器+显式JOIN inserted/deleted表+避免GETDATE()和NULL比较陷阱,否则日志漏行、时间戳重复或触发器报错中断主操作。

直接说结论:能做,但必须用 AFTER 触发器 + 显式 JOIN inserted/deleted 表 + 避开 GETDATE() 和空值陷阱,否则日志会漏行、时间戳重复、或触发器自己报错中断主操作。
SQL Server 必须用 AFTER 而不是 INSTEAD OF 或 BEFORE
INSTEAD OF 会拦截原始 DML,你看到的 inserted 和 deleted 是你手动构造的快照,不是数据库真实落盘前后的状态;BEFORE 在 SQL Server 中根本不存在。只有 AFTER INSERT, UPDATE, DELETE 能保证:主表约束已校验、外键已检查、事务上下文完整、且 inserted/deleted 真实反映变更结果。
- 一次
UPDATE影响 500 行,AFTER触发器只调用一次,inserted和deleted各含 500 行;INSTEAD OF下你得自己循环插入,极易出错 - 不要为同一张表建三个独立触发器(
AFTER INSERT、AFTER UPDATE、AFTER DELETE),否则无法统一判断复合操作类型(比如某次语句同时含 INSERT 和 UPDATE) - 触发器里禁止再对原表做
INSERT/UPDATE/DELETE,否则可能递归触发或死锁
UPDATE 场景下必须 JOIN inserted 和 deleted,不能靠行序或 COUNT
inserted 和 deleted 是无序集合,没有“第1行对应第1行”的保证。UPDATE 时旧值在 deleted,新值在 inserted,二者必须通过主键或唯一键显式关联,否则会漏数据或笛卡尔积。
- 正确写法:
SELECT i.id, d.email AS old_email, i.email AS new_email FROM inserted i INNER JOIN deleted d ON i.id = d.id - 错误写法:
SELECT (SELECT TOP 1 email FROM deleted), (SELECT TOP 1 email FROM inserted)—— 只取一行,批量操作必丢数据 - 如果主键缺失或不唯一,
ON i.id = d.id会失败或产生多对一匹配,导致日志重复或错位 -
inserted和deleted在 INSERT/DELETE 时另一张表为空,但结构仍存在,不可SELECT * FROM deleted(INSERT 场景下会报错)
时间戳必须用 SYSUTCDATETIME(),别碰 GETDATE()
GETDATE() 返回语句开始时间,高并发下多个触发器可能拿到完全相同的时间戳(毫秒级),排序或去重时无法区分先后;更糟的是它依赖服务器本地时区,跨地域部署会乱。
- 推荐方案:日志表字段设默认值
DEFAULT SYSUTCDATETIME(),触发器INSERT时直接省略该列 - 若需显式赋值,用
SYSUTCDATETIME(),精度到 100ns,且是 UTC,避免时区偏移 - 绝对不要在触发器里调用
GETUTCDATE()多次——它和GETDATE()一样有精度缺陷,只是时区对了 - 日志表字段类型必须是
DATETIME2(3)或更高,DATETIME只有 3.33ms 精度,不够区分密集变更
NULL 比较必须用 IS DISTINCT FROM,别用 != 或 =
在 MySQL/PostgreSQL 中,OLD.email != NEW.email 遇到任一端为 NULL 时返回 UNKNOWN,整个 IF 判断失效;SQL Server 虽不支持该语法,但在 JOIN 条件或 WHERE 过滤中直接用 = 比较 NULL 也会跳过。
- MySQL/PostgreSQL:用
OLD.email IS DISTINCT FROM NEW.email,天然处理两边 NULL 相等的情况 - SQL Server:改用
(i.email d.email) OR (i.email IS NULL AND d.email IS NOT NULL) OR (i.email IS NOT NULL AND d.email IS NULL) - 审计日志里记录字段值时,别直接
CONVERT(VARCHAR, d.email),NULL 会变空字符串,掩盖真实变更;用ISNULL(CONVERT(VARCHAR, d.email), 'NULL')显式标出 - 触发器开头必须加
SET NOCOUNT ON,否则客户端可能因额外结果集(如 “(1 row affected)”)中断连接
最易被忽略的一点:日志表本身不能有外键指向主表,也不能建在主表所在文件组上——否则主表大事务锁表时,日志插入会被阻塞,进而拖垮整个业务 DML。分区和聚集索引要提前规划,别等日志表涨到几亿行才想起优化。

















