SQL Server中获取Inserted和Deleted表数据必须在AFTER触发器内按操作类型判断:仅inserted有数据为INSERT,仅deleted有数据为DELETE,两者均有为UPDATE;二者均为内存虚拟表,结构同原表,触发器结束后自动销毁。

能,但必须手动建日志表、分操作类型处理 inserted 和 deleted 虚拟表,并用 AFTER 触发器——SQL Server 不支持单触发器自动识别 INSERT/UPDATE/DELETE 类型。
必须先创建结构合理的日志表
日志表不能只存时间戳和操作类型,否则查不出“谁改了哪条数据”。关键字段缺一不可:
-
table_name(NVARCHAR(64)):硬编码写死会失去复用性,建议用OBJECT_NAME(@@PROCID)或显式传入 -
operation(NVARCHAR(10)):必须区分'INSERT'、'UPDATE'、'DELETE',不能用模糊值如'MODIFY' -
old_data和new_data(NVARCHAR(MAX)):SQL Server 没原生 JSON 类型(2022+ 才有),别用TEXT(已弃用),统一用NVARCHAR(MAX)存序列化字符串 -
changed_by(NVARCHAR(128)):用SYSTEM_USER,不是CURRENT_USER(后者可能被EXECUTE AS覆盖) -
change_time(DATETIME2(3)):比DATETIME精度高,且避免GETDATE()在事务中被缓存的问题
漏掉 old_data 字段,DELETE 和 UPDATE 就只剩操作类型,审计时无法回溯原始值。
AFTER 触发器里怎么判断是 INSERT/UPDATE/DELETE?
不能靠 CASE WHEN EXISTS(SELECT * FROM inserted)... 这种写法——它在并发高或空批量操作时可能误判。正确逻辑是:
- 仅
inserted有数据 →INSERT - 仅
deleted有数据 →DELETE - 两者都有 →
UPDATE
示例片段:
DECLARE @op NVARCHAR(10);
IF EXISTS(SELECT 1 FROM inserted) AND NOT EXISTS(SELECT 1 FROM deleted)
SET @op = 'INSERT';
ELSE IF EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted)
SET @op = 'DELETE';
ELSE IF EXISTS(SELECT 1 FROM inserted) AND EXISTS(SELECT 1 FROM deleted)
SET @op = 'UPDATE';
注意:UPDATE 场景下必须做字段级变更检测(比如只更新了 email,但触发器把整行都记为“变更”),否则日志爆炸。可用 CHECKSUM() 或逐字段比对,但别在触发器里 SELECT * FROM inserted i JOIN deleted d ON i.id = d.id WHERE i.col != d.col —— 这会拖慢主事务。
为什么 DELETE 必须用 BEFORE 是错的?
SQL Server **没有 BEFORE DELETE 触发器**。它的 DML 触发器只有 AFTER 和 INSTEAD OF 两种。所以:
-
DELETE只能用AFTER DELETE,此时deleted表仍可读,没问题 -
INSTEAD OF DELETE虽然能提前拦截,但你要自己实现删除逻辑,还容易绕过约束,不推荐用于审计
常见错误是抄 MySQL 写法,以为 SQL Server 也支持 BEFORE,结果语法报错:Incorrect syntax near 'BEFORE'。记住:SQL Server 的 deleted 表在 AFTER DELETE 中完全可用,无需提前。
最容易被忽略的性能雷区
触发器运行在主事务上下文中,任何阻塞都会卡住业务 SQL。以下操作在触发器里等于埋雷:
- 调用
sp_OACreate或OPENROWSET访问外部系统(禁用Ole Automation Procedures后直接失败) - 对大表执行
SELECT COUNT(*) FROM audit_log做日志量统计 - 在触发器里再调用含
WAITFOR或长事务的存储过程 - 把
inserted/deleted直接SELECT ... FOR XML AUTO序列化——XML 生成开销大,且无法索引
真正轻量的做法是:用 CONCAT 或字符串拼接构造简易键值对(如 'id=' + CAST(i.id AS VARCHAR) + ',name=' + ISNULL(i.name, '')),不依赖复杂序列化。如果后续要查某字段变更,就靠应用层解析,别指望在触发器里做全文检索。

















