sql server触发器中通过inserted和deleted虚拟表获取修改前后整行数据:inserted存insert/update后新值,deleted存delete/update前旧值;after update触发器需inner join二者,不可查原表取旧值。

SQL Server触发器里怎么拿到修改前后的整行数据
直接用 inserted 和 deleted 这两张“虚拟表”——它们不是真实表,不占磁盘空间,但每次触发时由引擎自动填充,内容就是本次 DML 操作涉及的行快照。
关键点:
-
inserted包含 INSERT 或 UPDATE 后的新值;UPDATE 时它和原表当前值一致 -
deleted包含 DELETE 或 UPDATE 前的旧值;UPDATE 时它和原表更新前状态一致 - 这两张表只在触发器执行期间存在,不能跨触发器访问,也不能在普通查询中引用
- 批量操作(比如一次 UPDATE 100 行)会把全部变更行塞进同一张
inserted或deleted表,不是逐行触发
AFTER UPDATE 触发器中同时读取新旧数据的写法
必须用 AFTER UPDATE,因为只有这时 inserted 和 deleted 都有数据。别用 BEFORE——SQL Server 没这玩意儿;也别用 INSTEAD OF,除非你真打算自己重写整个 UPDATE 逻辑。
典型结构是靠 JOIN 关联两张虚拟表,用主键对齐:
CREATE TRIGGER tr_audit_users_update ON users AFTER UPDATE AS BEGIN SET NOCOUNT ON; <p>INSERT INTO users_audit (user_id, old_data, new_data, operated_at, login_name) SELECT i.id, (SELECT <em> FROM deleted d WHERE d.id = i.id FOR JSON AUTO), (SELECT </em> FROM inserted i2 WHERE i2.id = i.id FOR JSON AUTO), GETDATE(), ORIGINAL_LOGIN() FROM inserted i INNER JOIN deleted d ON i.id = d.id; END;</p>
注意:
- 这里用
FOR JSON AUTO把整行转成 JSON 字符串存下来,避免字段增减导致备份表结构频繁改;不用CONVERT(NVARCHAR(MAX), *),那在 SQL Server 2016+ 会报错 - 必须显式
INNER JOIN,否则多行更新时可能笛卡尔积 - 如果主键不是单一列(比如复合主键),
ON条件要补全所有字段
为什么不能在触发器里查原表来获取旧值
因为 AFTER UPDATE 已经提交了变更,此时查原表得到的是新值,不是旧值。常见错误写法:
-- ❌ 错误:试图从原表读旧值 SELECT * FROM users WHERE id IN (SELECT id FROM inserted); -- 返回的是更新后数据
后果是日志里新旧值一模一样,完全失去审计意义。唯一可靠来源只有 deleted 表。
另外两个坑:
-
deleted在 INSERT 触发器里为空,inserted在 DELETE 触发器里为空,别忘了加IF EXISTS(SELECT * FROM deleted)判断 - 如果原表有计算列、默认约束或触发器自身又改了别的表,
inserted/deleted仍只反映本次语句直接影响的字段值,不包含后续级联结果
备份字段类型和 NULL 处理容易被忽略
存整行 JSON 是最省事的方案,但如果你坚持用宽表方式存字段,要注意三件事:
- 原表字段为
datetime2(7),备份表对应列必须同精度,否则秒后小数位被截断 - 原表某列允许
NULL,备份表对应列也得允许,不然插入失败 - 原表用了
ROWVERSION或IDENTITY,这些值在inserted/deleted里是真实存在的,但备份表别给它设IDENTITY属性,否则插入会冲突
最后提醒一句:触发器里的操作属于原事务一部分,一旦审计插入失败(比如备份表磁盘满、字段长度超限),整个 UPDATE 会回滚。线上用之前务必压测高并发更新场景下的稳定性。










