SQL触发器在批量操作中只执行一次,而非每行一次;inserted/deleted表是完整结果集,须用JOIN等集合操作处理,避免单值赋值或游标;禁用触发器仅适用于无需业务逻辑的运维场景。

SQL触发器只执行一次,不是每行一次
批量更新时触发器不会为每一行单独触发——这是最常被误读的前提。比如 UPDATE orders SET status = 2 WHERE id IN (1001, 1002, 1003) 影响 3 行,触发器仍只运行 1 次,inserted 表里会一次性包含这 3 行新值。你写 SELECT @id = id FROM inserted,@id 最终只拿到其中某一行(顺序不确定),其余两行就丢了。
- 必须把
inserted当作集合处理:用JOIN、EXISTS或聚合函数,而不是单值赋值 - MySQL 的
NEW在FOR EACH ROW触发器中确实是单行别名,但 SQL Server 和大多数其他引擎的inserted/deleted是完整结果集 - 验证是否真被批量触发:在触发器里加日志,如
INSERT INTO debug_log SELECT 'update', COUNT(*) FROM inserted
用 JOIN 替代游标做批量关联更新
需要把 inserted 中的数据同步到另一张表?直接 JOIN 是最快最安全的方式。游标遍历看似“逐行可控”,实则性能差、易死锁、难维护。
- ✅ 正确:
UPDATE t SET t.flag = i.new_flag FROM target_table t INNER JOIN inserted i ON t.id = i.id - ❌ 错误:
DECLARE @id INT; SELECT @id = id FROM inserted; UPDATE target_table SET flag = 1 WHERE id = @id(只处理一行) - ⚠️ 游标仅在极少数必须调外部存储过程或需复杂状态判断时才考虑,且要加
WITH (NOLOCK)避免阻塞 - 如果目标表有唯一约束冲突,先用
MERGE(SQL Server)或INSERT ... ON CONFLICT(PostgreSQL)替代单纯UPDATE
按字段值分流处理必须写进触发器主体
SQL 不支持“根据每行 status 值自动路由到不同回调”。所谓“行级回调”,本质是你自己在触发器里写分支逻辑,数据库不会帮你分发。
- 用
IF EXISTS (SELECT 1 FROM inserted WHERE status = 1)判断是否存在某类数据,再用INSERT INTO #temp SELECT * FROM inserted WHERE status = 1分离子集 - 不要在触发器里循环
EXEC sp_executesql调用存储过程——每次 EXEC 都是新会话开销,N 行 = N 次编译 + N 次锁竞争 - MySQL 更严格:BEFORE/AFTER 触发器中禁止对本表做 DML,也禁止调用含动态 SQL 的存储过程;想分流只能靠日志表 + 外部程序消费
- 真正可扩展的方案是:触发器只做轻量记录(如写
change_log),由独立服务按row_id查原始数据后决定后续动作
大批量操作前该不该禁用触发器
禁用触发器(ALTER TABLE tbl DISABLE TRIGGER ALL)不是“优化”,而是绕过业务逻辑的临时手段——它快,但也危险。
- 适合场景:ETL 导入、灾备恢复、历史数据迁移等明确不需要触发业务侧逻辑的运维操作
- 不适合场景:日常业务批量更新(如运营活动发券)、状态批量变更(如订单发货)——禁用等于跳过校验、审计、同步等关键环节
- 禁用后务必配对启用,且建议用事务包裹:
BEGIN TRAN; ALTER TABLE ... DISABLE; BULK INSERT/UPDATE; ALTER TABLE ... ENABLE; COMMIT - 更稳妥的替代:在触发器开头加开关字段(如
WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE key = 'skip_trigger' AND value = '1')),用配置控制而非 DDL 操作
触发器里的集合思维比语法细节更重要:你面对的不是“一行数据”,而是“一个结果集”。一旦默认它是一次性装入的内存表,很多看似诡异的行为(比如取不到全部 ID、子查询报错)就立刻有解了。真正难的从来不是怎么写,而是怎么不写——把复杂分支和外部调用移出触发器,才是长期稳定的关键。

















