SQL Server和MySQL的DML触发器均为语句级,只执行一次,INSERTED/DELETED包含所有变更行;误用单值赋值或游标处理会导致逻辑错误、性能下降;BEFORE INSERT不被REPLACE/ON DUPLICATE触发;修改NEW字段会覆盖原值,且须全覆盖避免NULL。

触发器只执行一次,不是每行一次
SQL Server 和 MySQL 的 DML 触发器都是语句级(statement-level)的,不是行级(row-level)。这意味着 UPDATE table SET x = 1 WHERE id IN (1,2,3,4,5) 即使影响 5000 行,触发器也只被调用一次,INSERTED 和 DELETED 临时表里会一次性装入全部变更行——不是逐行进、逐行触发。
常见误解是“更新多少行就触发多少次”,结果在触发器里写 SELECT TOP 1 ... FROM INSERTED 或直接赋值给单个变量,导致只处理了第一条记录。比如:
DECLARE @id INT; SELECT @id = id FROM INSERTED; -- 若 INSERTED 有 N 行,@id 取哪一行?不确定!
正确做法是把 INSERTED 当作集合来处理,用集合操作(JOIN、EXISTS、子查询)或显式游标遍历。
MySQL 中 REPLACE INTO / ON DUPLICATE KEY UPDATE 不触发 BEFORE INSERT
大批量导入常用 REPLACE INTO 或 INSERT ... ON DUPLICATE KEY UPDATE,但它们在 MySQL 8.0.19 之前**不会触发 BEFORE INSERT**,只可能触发 BEFORE UPDATE(如果存在重复键)。这会导致你写的校验逻辑完全失效。
验证是否真被触发,最可靠方式是加日志:
INSERT INTO debug_log (ts, event, row_count) SELECT NOW(), 'before_insert', COUNT(*) FROM INSERTED;
若日志没记录,先别查逻辑,先查:SHOW TRIGGERS LIKE 'your_table'; 确认 Timing 是 BEFORE 且 Event 匹配你的实际语句类型。
规避方案:
- 改用标准
INSERT+ON CONFLICT(PostgreSQL)或MERGE(SQL Server) - 批量操作前统一走存储过程,内部用
INSERT ... SELECT避免隐式语义切换 - MySQL 下强制走
INSERT路径:先DELETE再INSERT,确保触发器时机可控
SQL Server 游标遍历 INSERTED 是权宜之计,不是推荐解法
看到“大批量更新触发器只跑一次”,很多人立刻想到用游标逐条处理 INSERTED,比如:
DECLARE cur CURSOR FOR SELECT id, amount FROM INSERTED; OPEN cur; FETCH NEXT FROM cur INTO @id, @amount; WHILE @@FETCH_STATUS = 0 BEGIN UPDATE other_table SET balance = @amount WHERE ref_id = @id; FETCH NEXT FROM cur INTO @id, @amount; END
这能工作,但问题明显:
- 游标性能差,1000 行以上就明显变慢,且阻塞时间拉长
- 无法利用 SQL Server 的集合优化能力,本可一条
UPDATE ... FROM INSERTED完成的事,硬拆成 N 次单行更新 - 事务范围扩大,失败时回滚代价高;若中途出错,已更新的部分难追溯
更合理的方式是用集合操作重写逻辑:
UPDATE t SET t.balance = i.amount FROM other_table t INNER JOIN INSERTED i ON t.ref_id = i.id;
只有当业务逻辑**必须依赖上一行结果**(如累计求和、状态机流转)时,才考虑游标或临时表 + 排序 + 窗口函数。
触发器里改 NEW.xxx,外部 UPDATE 的值就作废了
在 BEFORE UPDATE 触发器中,对 NEW.column 赋值,会直接覆盖原始 SQL 语句中指定的值。例如:
UPDATE users SET status = 'active', updated_at = NOW() WHERE id = 123;
若触发器里写了 SET NEW.status = 'pending';,最终写入的 status 就是 'pending',而不是语句里的 'active'。
这个特性常被误用为“兜底校验”,但容易掩盖上游错误。尤其在批量更新时,如果你只在部分条件下改 NEW.status,而其他分支没写,就会导致字段被设为 NULL(取决于字段是否允许空)。
关键提醒:
-
AFTER UPDATE中不能修改NEW,会报错Can't update table 't' in stored function/trigger - 所有分支都必须显式赋值,避免遗漏;不要依赖“不写就是保持原值”
- 批量场景下,
NEW是整张临时表,SET NEW.x = ...实际是隐式对全集广播,要小心条件范围
真正棘手的是跨表联动逻辑——你以为只是改一行,结果触发器又去更新另一张表,而那张表也有触发器……这种链式调用在大批量时极易锁表或超时,必须提前压测并限制单批数据量。










