merge语句在sql server中按实际执行的dml类型(insert/update/delete)分别触发对应触发器,每次仅触发一次,但inserted/deleted表可能包含多行;需用集合操作而非标量赋值,防范递归触发与事务膨胀。

SQL Server中MERGE触发的是单次还是多次触发器
MERGE语句在SQL Server中只触发一次对应类型的触发器(AFTER INSERT、AFTER UPDATE、AFTER DELETE),不是按行多次触发。它会根据操作类型,把实际影响的行分别填入inserted和deleted伪表:INSERT分支走inserted,UPDATE分支同时填充inserted和deleted,DELETE分支走deleted。这和单独执行多条INSERT/UPDATE/DELETE语句有本质区别。
为什么你看到“多次触发”其实是误判
常见误判来源有三个:
- 把批量MERGE中
inserted或deleted里多行当成“触发了多次”,其实只是单次触发器内处理多行——这是正常且预期的行为 - 触发器内部又对同一张表执行DML(比如UPDATE目标表后又INSERT日志到同表),导致二次触发(递归触发),尤其当
RECURSIVE_TRIGGERS数据库选项开启时 - 在主从复制环境里,主库执行一次MERGE,从库重放binlog时再执行一遍触发器,造成“双写”假象
MERGE触发器必须用集合式逻辑,禁用标量赋值
一旦误用SELECT @id = id FROM inserted这类写法,在MERGE涉及1000行时只会取到其中一行(执行计划决定),其余数据静默丢失。正确做法是面向集合:
- 校验逻辑用
NOT EXISTS (SELECT 1 FROM inserted i WHERE i.id = t.id AND i.status NOT IN ('valid'))批量拦截 - 关联更新用
UPDATE t SET flag = 1 FROM target t INNER JOIN inserted i ON t.id = i.id - 审计写入用
INSERT INTO audit_log (...) SELECT i.id, i.status, GETDATE() FROM inserted i - 需要区分INSERT/UPDATE分支时,用
IF EXISTS(SELECT 1 FROM inserted i JOIN deleted d ON i.id = d.id)判断交集存在性
真正棘手的是MERGE + 触发器 + 多表联动的隐式事务膨胀
触发器里调用存储过程、写通知表、查维表、甚至跨库链接服务器,都会把原本轻量的MERGE事务拖成大事务。更隐蔽的问题是:MERGE本身不保证各分支执行顺序,而触发器又共享外部事务上下文,一旦某个分支失败,整个MERGE回滚,但触发器已执行的部分(如发消息、写日志)可能无法撤回。
安全边界要划清:触发器只做原子级状态标记(如SET is_synced = 0),耗时或跨系统动作一律推到异步队列;如果业务强依赖同步响应,MERGE就不该进触发器,改由应用层分步控制。










