<p>SQL Server和MySQL的DML触发器是语句级而非行级,批量插入只触发一次,INSERTED/NEW中包含全部新行;错误地用标量变量赋值会丢失数据,正确做法是集合操作如INSERT INTO log SELECT * FROM INSERTED。</p>

SQL Server 和 MySQL 的触发器只执行一次,不是逐行触发
很多人以为 INSERT INTO t VALUES (1),(2),(3) 会调用触发器三次,实际只调一次,INSERTED(SQL Server)或 NEW(MySQL)里是全部三行的集合。错误写法如 SELECT @id = id FROM INSERTED 只取最后一行,其余数据丢失。
- 正确做法是用集合操作:比如
INSERT INTO log SELECT * FROM INSERTED或UPDATE t2 SET x = 1 FROM t2 JOIN INSERTED i ON t2.id = i.id - 避免用标量变量赋值、游标或 while 循环处理
INSERTED——性能差、易死锁、难维护 - SQL Server 中游标/临时表方案虽能“模拟”逐行逻辑,但本质是把单次触发拆成多次手动遍历,违背触发器设计初衷,且并发下可能出错
SqlBulkCopy 默认不触发任何触发器
SqlBulkCopy 是绕过 T-SQL 执行管道的底层写入,不走约束检查、不进触发器、甚至跳过部分日志记录。这不是 bug,是设计选择——为速度牺牲完整性保障。
- 启用触发器只需一行:
bulkCopy.FireTriggers = true;,但会显著降低吞吐量(实测常降 30%~50%) - 更推荐的做法:先
BULK INSERT到临时表,再用INSERT INTO real_table SELECT * FROM #staging—— 触发器生效,且可控性强 - 注意:
FireTriggers = true对BULK INSERTT-SQL 命令无效,仅对SqlBulkCopy类起作用
PostgreSQL 的 FOR EACH STATEMENT + NEW TABLE 是真批量友好方案
PostgreSQL 9.4+ 支持 FOR EACH STATEMENT 触发器,并可通过 NEW TABLE 直接访问本次语句影响的全部新行,无需循环或临时表。
- 例如统计汇总:
INSERT INTO summary_cache SELECT status, COUNT(*) FROM NEW TABLE GROUP BY status,一次聚合代替 N 次单行 UPDATE - MySQL 没有
NEW TABLE,只能退而求其次:触发器只写轻量日志表(如trigger_log),再由外部工具(Canal / Debezium)消费后批量更新 - 别在触发器里做
SELECT ... FOR UPDATE或跨库写操作——锁范围扩大、事务膨胀、超时风险陡增
BULK INSERT / LOAD DATA INFILE 从不触发 DML 触发器
这是硬性限制,不是配置问题。BULK INSERT(SQL Server)、LOAD DATA INFILE(MySQL)属于高速数据泵,完全绕开 DML 引擎,自然也不经过触发器路径。
- 验证很简单:在触发器里加
INSERT INTO debug_log,执行BULK INSERT后查debug_log为空 → 就是它没被调用 - 替代路径只有两条:① 改用常规
INSERT ... SELECT;② 导入后补触发逻辑(如跑一个UPDATE summary SET cnt = (SELECT COUNT(*) FROM t WHERE ...)) - 注意:TRUNCATE 是 DDL,同样不激活任何 DML 触发器;UPDATE 若字段值未真正变更(如
SET name = name),MySQL 8.0+ 也会跳过触发器
触发器里的批量逻辑最容易被忽略的点,是默认行为和“看起来像逐行”的错觉之间的偏差——它既不是每行一次,也不是完全不管批量,而是在语句粒度上交给你一个行集。怎么用这个集,决定了你是省事还是埋雷。

















