SQL Server触发器默认与原DML语句共用同一事务,故AFTER触发器中INSERT INTO历史表会随主操作回滚;需用TRY...CATCH+@@TRANCOUNT判断或Service Broker解耦事务。

触发器里不能直接 INSERT INTO 历史表?先确认事务上下文
SQL Server 触发器默认运行在引发它的 DML 语句同一事务中,INSERT / UPDATE / DELETE 操作失败时,触发器里写的 INSERT INTO history_table 也会回滚——这看似“安全”,但对快照表来说反而是陷阱:你本想保留变更前的状态,结果连历史记录也丢了。
真正可用的做法是把历史写入剥离出原事务,常见手段只有两种:sp_getapplock 配合异步轮询(复杂且易堵),或更稳妥的 Service Broker + 激活存储过程。但多数业务场景其实不需要强一致性历史,用 AFTER 触发器 + TRY...CATCH 包裹写历史逻辑,再加 IF @@TRANCOUNT > 0 判断是否仍在原事务里,能避开 90% 的误清空问题。
INSERTED/DELETED 表结构必须和历史表严格对齐?字段顺序和 NULL 性都要匹配
很多人以为只要列名一样就能 INSERT INTO history SELECT * FROM INSERTED,结果报错 Column name or number of supplied values does not match table definition。根本原因是:INSERTED 和 DELETED 是内存临时表,其列顺序、是否允许 NULL、默认值行为完全继承源表定义,而历史表若手工建表或后期加过字段,极易出现错位。
实操建议始终显式列出字段:
INSERT INTO order_history (order_id, status, updated_at, op_type) SELECT i.order_id, i.status, GETDATE(), 'U' FROM INSERTED i INNER JOIN DELETED d ON i.order_id = d.order_id WHERE i.status != d.status;
特别注意:GETDATE() 写入时间比 SYSUTCDATETIME() 更稳妥,避免跨时区应用读历史时时间倒挂;op_type 字段强烈建议加上,否则无法区分是新增、修改还是删除产生的快照。
UPDATE 触发器里怎么捕获“有效变更”而非所有 UPDATE?别依赖 WHERE 子句过滤
直接在触发器里写 WHERE status != 'processed' 是错的——它只过滤行,不判断字段是否真变了。比如一行数据被 UPDATE SET updated_at = GETDATE(),其他字段没动,你也存了一条重复快照,历史表迅速膨胀。
正确做法是逐字段比对 INSERTED 和 DELETED:
- 用
CHECKSUM(*)粗筛(注意:NULL 值导致校验和恒为 NULL,需预处理) - 或更可靠:对关键业务字段显式比较,如
i.amount != d.amount OR i.customer_id != d.customer_id - 字符串字段记得用
ISNULL(i.name, '') != ISNULL(d.name, ''),否则NULL != NULL返回UNKNOWN,整行被跳过
性能敏感场景下,可考虑在主表加 rowversion 列,触发器里只比对 i.rowversion != d.rowversion——但这只能说明“行被改过”,不能定位哪列变了。
历史表爆满怎么办?分区切换(Partition Switch)比 DELETE 更安全
用 DELETE FROM history_table WHERE created_at 清理旧数据,容易锁表、阻塞查询、产生大量日志。SQL Server 的 <code>SWITCH PARTITION 是原子操作,毫秒级完成,前提是历史表已按时间分区(如每月一个分区)。
关键步骤只有三步:
- 新建一个空的、结构完全相同的分区表(
CREATE TABLE history_staging ...) - 执行
ALTER TABLE history_table SWITCH PARTITION 3 TO history_staging(假设第 3 分区是 2023 年 1 月) - 对
history_staging执行DROP TABLE或归档到其他库
注意:SWITCH 要求源表和目标表的索引、约束、统计信息全部一致,哪怕一个索引名称不同都会失败。建议用 sys.dm_db_partition_stats 核对分区行数,避免“以为切走了,其实没切成功”的静默故障。
最常被忽略的是:分区函数的边界值必须和历史数据实际时间对齐。比如用 RANGE RIGHT FOR VALUES ('2023-01-01', '2023-02-01'),那 2023-01-01 00:00:00 的数据会进第二个分区,不是第一个——这个偏移量一旦设错,后续所有切换都错位。

















