触发器逐行执行会放大锁竞争,因每行变更都触发一次完整事务流程,导致锁堆积、日志膨胀和死锁风险上升;正确做法是用集合操作(如join)处理inserted表,而非单行赋值。

为什么触发器逐行执行会放大锁竞争
触发器本身不“慢”,但高频 UPDATE 触发时,inserted 表每来一行,就执行一次触发器逻辑——哪怕你只改 10 万行,它就被调用 10 万次。每次调用都开事务、加锁、写日志、重编译执行计划。InnoDB 的行锁在触发器里不是被“释放”了,而是被“重复申请+堆积”。更糟的是,如果触发器里还 UPDATE 关联表,那锁链直接从 1 行拉成 N 行 × M 表,死锁概率飙升。
用批量 JOIN 替代单行赋值是硬性前提
SQL Server 触发器中,inserted 是表,不是变量。下面这种写法注定出错:
SELECT @id = id FROM inserted; -- ❌ 只取一行,其余丢弃 UPDATE t2 SET status = 'done' WHERE id = @id;
正确做法是把 inserted 当作普通表参与集合运算:
UPDATE t2 SET status = 'done' FROM t2 INNER JOIN inserted i ON t2.id = i.id- 若需聚合(如累加订单子项金额),必须
GROUP BY目标表主键,并显式列出所有被更新列 - 避免在触发器里用
EXISTS (SELECT ... FROM inserted WHERE ...)做条件判断——它可能漏掉部分行的业务约束
MySQL 用户别碰本表 UPDATE,绕开比硬刚靠谱
MySQL 在触发器里 UPDATE 同一表会直接报 ERROR 1442,这不是锁问题,是执行栈保护机制。强行绕过(比如用临时表、视图、子查询)大概率失败,且不可维护。
可行路径只有三条:
- 触发器里只写轻量事件(如插入
event_queue记录),由后台任务异步批量处理关联更新 - 把逻辑提到应用层:用一个事务包住原始
INSERT/UPDATE和后续关联UPDATE,可控、可测、可重试 - 运行时标记跳过:在触发器开头加
IF @skip_trigger IS NULL THEN ... END IF,应用侧在关键语句前设变量,但要注意事务边界和连接复用问题
分批 + 显式 COMMIT 是缓解日志与锁压力的底线操作
即使触发器逻辑已优化,批量操作仍会撑爆事务日志、拖长锁持有时间。必须拆批次,且每批后 COMMIT:
- 优先用主键范围分片:
WHERE id BETWEEN ? AND ?,确保索引命中、无遗漏、无重叠 - 慎用
LIMIT分页:MySQL 的UPDATE ... LIMIT不支持ORDER BY,易漏数据;若必须用,配合ROW_COUNT()判断退出 - 每批大小建议 2000–5000 行;字段宽或日志压力大时,往下调至 1000
- 批间加
WAITFOR DELAY '00:00:00.1'(SQL Server)或SLEEP(0.1)(MySQL),缓解 I/O 尖峰
真正难处理的从来不是语法怎么写,而是哪部分逻辑“必须强一致”、哪部分可以接受几十毫秒延迟——这个判断一旦错,所有优化都在给错误假设打补丁。










