SQL Server和MySQL的DML触发器是集合操作而非逐行处理——inserted/deleted是表,变量赋值会静默覆盖多行数据,正确做法是用JOIN等集合操作处理。

SQL Server 和 MySQL 的 DML 触发器根本就不是为“逐行处理”设计的——它只执行一次,inserted 和 deleted 里装着全部变更行;所谓“只处理一行”,几乎全是误把集合当单值用导致的逻辑错位。
为什么 SELECT @id = id FROM inserted 只拿到一行
这是最典型的误解源头。SQL Server 允许你写 SELECT @id = id FROM inserted,但它不报错不代表语义正确:当 inserted 有 1000 行时,该语句会逐行赋值,@id 最终只保留最后一行的值,其余 999 行被静默覆盖。MySQL 中类似写法(如 SET @id = NEW.id)在多行 INSERT 下也只反映最后一次触发的那行。
- 这不是随机,是确定性覆盖:SQL 标准规定标量变量只能存一个值
- 调试时单行测试正常,一上批量就出问题,就是因为没暴露多行场景
- 用
TOP 1或ORDER BY强行取某行,本质仍是丢数据,不是修复
UPDATE 关联目标表时只更新了一条记录
常见错误是写成 UPDATE t SET x = (SELECT amount FROM inserted),子查询返回多行直接报错 Subquery returned more than 1 value;或者退而求其次用 WHERE id IN (SELECT id FROM inserted),但优化器常因 inserted 无统计信息选错执行计划,实际只命中部分索引页。
- 正确做法是显式
JOIN:UPDATE t SET x = i.amount FROM t INNER JOIN inserted i ON t.id = i.id - 确保
t.id有索引,否则JOIN退化为嵌套循环,性能断崖下跌 - 若需条件过滤(如仅同步
status = 'shipped'的订单),加WHERE i.status = 'shipped'在JOIN后,提前减少数据集
游标遍历 inserted 是性能毒药,不是解法
看到“只处理了一行”,第一反应写游标逐条处理 inserted,这在 SQL Server 里很常见,但属于反模式:它把本可并行的集合操作强行序列化,锁持有时间翻倍,事务日志膨胀,10 万行可能从秒级拖到分钟级。
- 游标无法利用索引下推、哈希连接等优化,纯靠 FETCH 循环
- 并发下
@@FETCH_STATUS不稳定,容易漏行或死循环 - 真正需要“每行不同逻辑”的场景(比如调不同外部 API),说明不该放在触发器里——该挪到应用层异步队列或定时批任务
BULK INSERT / SqlBulkCopy 根本不进触发器
很多“失效”不是触发器没跑,而是压根没被调用。SQL Server 的 BULK INSERT 和 .NET 的 SqlBulkCopy 是绕过完整 DML 执行栈的底层写入,默认跳过触发器、约束检查、甚至部分日志记录。
- 验证方式:在触发器里插一条
INSERT INTO debug_log,执行BULK INSERT后查日志表为空 → 就是这个原因 -
SqlBulkCopy.FireTriggers = true能启用,但吞吐量实测下降 30%~50% - 更可控的做法:先
BULK INSERT到临时表#staging,再INSERT INTO real_table SELECT * FROM #staging—— 触发器生效,且可加索引、分批次
核心陷阱在于:你试图用行级思维去驾驭语句级机制。真正安全的做法,是接受 inserted 就是一张表,然后用 JOIN、EXISTS、GROUP BY 这些本来就在数据库里高效运行的集合操作来处理它——而不是反复尝试把它“掰碎”。

















