在t-sql update触发器中,通过inner join关联inserted和deleted表(推荐用主键),再逐字段null安全比较(is null/is not null显式判断)可准确获取变更前后的值;需cast统一类型、防递归、确保权限,并注意空字符串与null的业务语义差异。

触发器里怎么拿到UPDATE前后的值
在 T-SQL 触发器中,INSERTED 和 DELETED 是两个只读临时表,分别保存 UPDATE 后的新值和 UPDATE 前的旧值(对 UPDATE 操作而言)。它们结构与原表一致,但不包含未修改的列——哪怕某列没变,只要它在 UPDATE 语句中被显式列出(比如 SET status = status),也会出现在 INSERTED 中。
关键点:不能直接用 SELECT * FROM INSERTED 就完事。必须和 DELETED 基于主键或唯一键做 JOIN,否则多行更新时会笛卡尔积。
常见错误现象:INSERTED 和 DELETED 行数不一致(其实是正常情况,比如单行 UPDATE 就各一行;但若漏 JOIN 条件,结果集爆炸);或者误以为 DELETED 在 INSERT 触发器里也有数据(它没有)。
- UPDATE 触发器中,
INSERTED和DELETED行数一定相等(除非触发器被禁用或存在并发干扰) - JOIN 必须使用能唯一标识行的列,推荐用主键(如
id),避免用可能为 NULL 或非唯一的字段(如name) - 如果表无主键,必须靠业务逻辑补全唯一约束字段组合,否则无法可靠比对
记录字段级变更的最小可行写法
不要试图在触发器里拼接 JSON 或大文本日志。先聚焦「哪些字段变了」和「变成什么样」。最轻量的方式是逐字段判断,用 CASE WHEN INSERTED.col DELETED.col OR (INSERTED.col IS NULL AND DELETED.col IS NOT NULL) OR (INSERTED.col IS NOT NULL AND DELETED.col IS NULL) 检查是否变化(NULL 安全比较必须显式写)。
示例:记录 users 表中 email 和 balance 的变更
CREATE TRIGGER tr_users_audit ON users
AFTER UPDATE
AS
BEGIN
INSERT INTO user_audit_log (user_id, field_name, old_value, new_value, updated_at)
SELECT
i.id,
'email',
CAST(d.email AS VARCHAR(255)),
CAST(i.email AS VARCHAR(255)),
GETDATE()
FROM INSERTED i
INNER JOIN DELETED d ON i.id = d.id
WHERE 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);
<p>INSERT INTO user_audit_log (user_id, field_name, old_value, new_value, updated_at)
SELECT
i.id,
'balance',
CAST(d.balance AS VARCHAR(20)),
CAST(i.balance AS VARCHAR(20)),
GETDATE()
FROM INSERTED i
INNER JOIN DELETED d ON i.id = d.id
WHERE i.balance d.balance
OR (i.balance IS NULL AND d.balance IS NOT NULL)
OR (i.balance IS NOT NULL AND d.balance IS NULL);
END;</p>
注意:CAST 是必须的,因为不同字段类型(如 INT、DATETIME、NVARCHAR)要统一存进 old_value/new_value(通常是 VARCHAR 类型);否则隐式转换可能失败或截断。
为什么不能在触发器里用TRY...CATCH捕获所有错误
触发器运行在事务上下文中,一旦出错(比如插入审计表时违反约束、磁盘满、死锁),整个 UPDATE 事务会回滚——这是设计使然,不是 bug。但很多人误以为加个 TRY...CATCH 就能让触发器“静默失败”,其实不行:
-
TRY...CATCH只能捕获严重级别 11–19 的错误;像 2627(主键冲突)、2601(唯一索引冲突)可被捕获,但 1205(死锁)会被直接抛出,触发器外层事务仍会回滚 - 即使你
CATCH住并RETURN,SQL Server 仍认为触发器执行异常,UPDATE 不生效 - 真正该做的是:确保审计表结构宽松(如
old_value足够长、允许 NULL)、索引精简、避免在触发器里调用远程服务或复杂计算
性能影响很实际:每行 UPDATE 都要额外写 1+N 条审计记录(N=变更字段数),如果高频更新大字段(如 TEXT 或 XML),I/O 和日志增长会明显拖慢主业务。
容易被忽略的边界情况
真实业务中,这几个点几乎每次都会漏掉,导致审计日志缺失或错乱:
- UPDATE 语句中字段值「看起来没变」但实际被重算过:比如
UPDATE t SET ts = GETDATE(),即使ts原值就是当前时间,INSERTED.ts和DELETED.ts仍大概率不同(毫秒级差异),应按需加精度截断再比对 - 触发器对自身表的递归调用:如果审计表也建了 UPDATE 触发器,且逻辑里又去改
users,可能触发无限循环;务必用TRIGGER_NESTLEVEL()控制深度(如IF TRIGGER_NESTLEVEL() > 1 RETURN) - 用户权限问题:执行 UPDATE 的账号必须对审计表有
INSERT权限,否则触发器报错,主操作失败;DBA 往往只给业务账号 DML 权限,忘了审计表 - 空字符串
''和NULL是两回事,但业务上常被等同处理;比对逻辑里要不要把''当NULL看,得看业务规范,不能只依赖 SQL 默认行为
最麻烦的不是语法,是当某天发现审计日志里缺了某次关键修改——往往是因为当时那条 UPDATE 恰好没走预期路径(比如用了 UPDATE ... FROM 语法,或触发器被禁用过),而不是代码写错了。










