SQL Server触发器只执行一次,inserted/deleted是多行内存表而非单行变量;正确做法是用JOIN、GROUP BY或聚合进行集合运算,禁用SELECT @var=col FROM inserted等标量赋值。

SQL Server触发器只触发一次,但inserted/deleted是多行表
SQL Server 的 AFTER UPDATE 触发器无论影响 1 行还是 10000 行,都只执行一次——这是设计使然,不是 bug。关键在于,inserted 和 deleted 不是标量变量,而是内存中结构与原表一致的**临时行集表**。
常见错误现象:
- 用
SELECT @id = ID FROM inserted取主键,结果只拿到某一行(SQL Server 不保证顺序),后续UPDATE只改了单条记录 - 写
IF UPDATE(Amount) BEGIN UPDATE T2 SET Amount = @newAmt ... END,看似响应了字段变更,实则只处理了inserted中“被选中”的那一行
根本原因:把集合当成了标量。触发器内部没有“逐行回调”机制,必须用集合操作一次性处理整个 inserted。
用 JOIN 替代赋值,用 GROUP BY 控制聚合粒度
要让触发器正确响应批量更新,所有逻辑必须基于 JOIN、GROUP BY 或聚合函数展开,不能依赖单值变量承接多行数据。
例如同步更新订单头金额:
UPDATE po SET SubTotal = po.SubTotal + ISNULL(SUM(idt.LineTotal), 0) FROM PurchaseOrderHeader po INNER JOIN inserted idt ON po.PurchaseOrderID = idt.PurchaseOrderID GROUP BY po.PurchaseOrderID, po.SubTotal;
注意点:
-
GROUP BY必须包含目标表中被更新的列(如po.SubTotal),否则报错或行为未定义 - 不能写
SET SubTotal += idt.LineTotal—— 这会尝试把多行LineTotal当作单个值加,直接报错 - 若需对每行做独立校验(如拒绝负金额),用
IF NOT EXISTS (SELECT 1 FROM inserted WHERE LineTotal ,而不是查单行
UPDATE 和 DELETE 触发器中 deleted 表的语义差异
deleted 在不同触发器类型中含义不同,直接影响 JOIN 写法是否成立:
-
UPDATE触发器:deleted存旧值,inserted存新值,两表行数相等、主键可一一对应 → 可安全INNER JOIN -
DELETE触发器:deleted存被删行,inserted为空 → 不能写FROM deleted d INNER JOIN inserted i,会返回空结果
典型误用:
-- ❌ 在 UPDATE 触发器里这样写,假设行序对应,但 SQL Server 不保证顺序 SELECT d.ID, d.Price AS OldPrice, i.Price AS NewPrice FROM deleted d, inserted i WHERE d.ID = i.ID;
应改为显式 JOIN:
SELECT d.ID, d.Price AS OldPrice, i.Price AS NewPrice FROM deleted d INNER JOIN inserted i ON d.ID = i.ID;
避免游标和循环,警惕性能与死锁风险
有人试图用游标遍历 inserted 来“模拟逐行处理”,这不仅慢,还容易引发阻塞甚至死锁:
- 游标隐式持有锁时间更长,尤其在大事务中
- 触发器内调用另一个含事务的存储过程,极易形成嵌套锁等待
- 每次 FETCH 都是一次额外开销,1000 行就是 1000 次上下文切换
真正需要“每行独立动作”的场景(如发消息、调外部 API),应把 inserted 数据先插入队列表,由后台作业异步消费——触发器只做轻量、原子、集合化的数据同步。
最常被忽略的一点:触发器逻辑一旦写成单行思维(比如用变量承接、用 IF 做行级判断),就等于默认放弃了批量能力;而 SQL Server 的执行引擎根本不会替你“自动展开”这种逻辑——它只忠实地执行你写的那一条 SELECT @x = col FROM inserted,然后停在那里。

















