应使用 inserted 与 deleted 表显式比对字段值是否真实变化,而非依赖 UPDATE() 或 COLUMNS_UPDATED();需处理 NULL 安全比较、主键 JOIN、边界列(如 IDENTITY/计算列)不可比等关键细节。

用 UPDATE() 会误判,它只看 SQL 语句里有没有写那列
UPDATE(Email) 返回 1 的唯一条件是:UPDATE 语句的 SET 子句中显式出现了 Email——哪怕你写的是 SET Email = Email 或 SET Email = NULL,它都返回真。值根本没变,但触发器照样执行。这在审计日志、联动更新等场景下会导致大量无效记录或逻辑错乱。
常见错误现象包括:
- 日志表里反复出现“用户邮箱已更新”,查数据发现前后都是
bob@example.com - 触发器调用外部存储过程发通知,结果每次改密码也顺带发一封“邮箱变更”邮件
- 计算列或级联更新间接导致某字段值变化,但
UPDATE(col)却返回0,漏监控
真正检测“值变了没”,必须比对 inserted 和 deleted 表
SQL Server 触发器中没有 OLD/NEW 别名,只能靠 inserted(新值)和 deleted(旧值)两张虚拟表做显式比对。关键点是 NULL 安全:直接用 = 或 != 在任一操作数为 NULL 时结果为 UNKNOWN,整个条件失效。
推荐写法(以 Status 列为例):
FROM inserted i JOIN deleted d ON i.id = d.id WHERE 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)
或者更简洁地用 IS DISTINCT FROM(SQL Server 2022+ 支持):
WHERE i.Status IS DISTINCT FROM d.Status
注意:
- 必须加主键或唯一键 JOIN 条件,否则多行更新时会笛卡尔积
- 批量操作下,
inserted和deleted行序不保证与原始语句一致,不能依赖顺序取“第一条” - 若表无主键,需用所有非计算列联合匹配,成本高,应避免
用 COLUMNS_UPDATED() 只适合快速跳过无关列,不替代值比对
COLUMNS_UPDATED() 返回位图,能一次性知道哪些列出现在 UPDATE 的 SET 中,适合做前置过滤。比如表有 20 列,你只关心其中 3 个,可用它先判断是否值得进入后续比对逻辑。
示例:检查第 3、5、9 列是否被 SET(假设表列顺序固定):
-- 第3列对应 bit 4(从右往左,第1位是1, 第2位是2, 第3位是4) -- 第5列对应 bit 16(2⁴) -- 第9列跨字节,需 SUBSTRING:第9位列ID=9 → 字节索引 = (9-1)/8 = 1(0-based),位偏移 = (9-1)%8 = 0 → bit 1 IF (COLUMNS_UPDATED() & 4 > 0) OR (COLUMNS_UPDATED() & 16 > 0) OR (SUBSTRING(COLUMNS_UPDATED(), 2, 1) & 1 > 0) BEGIN -- 进入 Status/Amount/Remark 字段值比对分支 END
但它和 UPDATE() 一样,不反映真实值变化。所以典型用法是:IF COLUMNS_UPDATED() & @mask > 0 THEN ... 做 inserted/deleted 值比对 ...
复杂点往往藏在边界:计算列、IDENTITY、NULL 处理和触发时机
最容易被忽略的不是怎么写,而是哪些情况根本比不了:
-
IDENTITY列、ROWVERSION列、计算列,UPDATE()和COLUMNS_UPDATED()永远返回0,哪怕它们随其他列更新而变 -
INSERT或DELETE触发器里没有成对的inserted/deleted,无法做“值变化”判断——只有UPDATE触发器才适用该模式 -
INSTEAD OF触发器中,inserted/deleted内容虽同AFTER,但原操作尚未执行,若你在里面又UPDATE同表,可能引发嵌套触发或死锁 - 用
ISNULL(i.col, '') != ISNULL(d.col, '')看似简单,但若字段是varchar(max)或含不可比较类型(如xml),会隐式转换失败或性能骤降

















