sql server和mysql的dml触发器是语句级而非行级,一条insert语句无论影响多少行都只触发一次,inserted/new包含全部新行;须用join处理多行,禁用变量赋值、in子查询及@@rowcount;replace和on duplicate key update不触发before insert;sqlbulkcopy默认不触发触发器,需显式启用。

SQL Server 和 MySQL 的 DML 触发器天生就是语句级的,不是行级的——批量 INSERT 只触发一次,是设计如此,不是 bug。
为什么只执行一次?
触发器响应的是「语句」,不是「行」。哪怕你写 INSERT INTO t VALUES (1),(2),(3),(4) 插入 4 行,或用 INSERT INTO t SELECT * FROM #temp 插入 10 万行,只要是一条 SQL 语句,AFTER INSERT 就只运行一次。此时 INSERTED(SQL Server)或 NEW(MySQL)里装着全部新行,而不是“逐行进来、逐行触发”。
常见误判现象:
- 日志表只插入一条记录 → 其实是你写了
INSERT INTO log VALUES (@id),而@id是单变量,多行时取值不可控 - 业务表只更新了第一行 → 源于
SELECT @id = id FROM INSERTED,SQL Server 不报错但结果随机 - 用
@@ROWCOUNT判断处理行数 → 它反映的是触发前语句影响行数,不是INSERTED中实际行数
SQL Server 中怎么安全处理所有行?
把 INSERTED 当成一张真实表来 JOIN,别用变量赋值或游标——除非你明确接受性能惩罚。
- 正确做法:
UPDATE t2 SET status = 'imported' FROM t2 INNER JOIN INSERTED i ON t2.id = i.id,前提是t2.id有索引 - 禁用写法:
WHERE id IN (SELECT id FROM INSERTED)—— 若id含NULL,整个条件恒假;大数据量下还可能超参数上限 - 统计行数请用:
(SELECT COUNT(*) FROM INSERTED),别信@@ROWCOUNT - 真要逐行调外部逻辑(如发邮件),说明不该放触发器里,该挪到应用层异步队列
MySQL 用户特别注意:REPLACE 和 ON DUPLICATE KEY UPDATE 不走 BEFORE INSERT
这是个更隐蔽的问题:你以为写了 BEFORE INSERT 校验邮箱格式,但用 REPLACE INTO users 或 INSERT INTO users ... ON DUPLICATE KEY UPDATE 时,这个触发器压根不执行——MySQL 8.0.19 之前完全跳过 BEFORE INSERT。
- 验证是否触发:在触发器开头加日志,比如
INSERT INTO debug_log(event, row_count) VALUES ('before_insert', ROW_COUNT()) - 可靠绕过方式:改用标准
INSERT+ON CONFLICT(PostgreSQL)、MERGE(SQL Server),或先DELETE再INSERT - 若必须用
REPLACE,校验逻辑得提前放到应用层,或用存储过程封装
SqlBulkCopy 导入时触发器根本没跑?
不是配置漏了,是它本来就不走触发器路径。SqlBulkCopy 绕过 T-SQL 引擎,直写数据页,所以默认不触发约束、默认值、也不触发任何触发器。
- 启用触发器:必须显式设置
bulkCopy.FireTriggers = true - 替代方案更推荐:先
SqlBulkCopy到临时表,再用INSERT INTO target SELECT * FROM #temp—— 这条INSERT会正常触发 - 别在
FireTriggers = true下直接 bulk 大表,IO 和锁压力会陡增,尤其带审计日志类逻辑时
真正容易被忽略的点是:**触发器是否被调用,和它是否“生效”,是两回事。** 你看到日志里有记录,不代表业务逻辑覆盖了所有行;你看到 INSERTED 有 5000 行,不代表你写的 SELECT TOP 1 或变量赋值能拿到全部数据。










