触发器在多行INSERT时只处理一行是设计使然,因INSERTED表为集合,用标量变量赋值会丢失数据;应改用JOIN INSERTED批量处理,避免游标与逐行思维。

触发器在多行 INSERT 时只处理一行,不是 bug,是设计使然——INSERTED 表天然就是集合,强行用标量变量或游标去“逐行读”,等于主动放弃 SQL 的集合能力,还埋下线上静默丢数据的隐患。
别用 @var = col FROM INSERTED 赋值
这是最常见也最危险的写法。只要 INSERT 多于一行,SELECT @id = id FROM INSERTED 就只保留某一行(通常是最后一条),其余全丢。校验、日志、关联更新全部失效。
- 现象:单行测试正常,批量导入时部分记录状态没更新、库存扣错、审计日志少一半
- 本质:SQL 标准规定标量变量只能存一个值,而
INSERTED是表,不是结果集 - 替代方案:所有逻辑都基于
JOIN INSERTED,比如UPDATE t SET status = 'done' FROM t INNER JOIN INSERTED i ON t.id = i.id
用 INSERT/UPDATE 配合 FROM INSERTED 批量处理
绝大多数业务逻辑(写日志、改状态、扣库存、同步统计)根本不需要“逐行”,一条集合语句就能干完,性能好、逻辑清、无遗漏。
- 写审计日志:
INSERT INTO audit_log (table_name, record_id, action) SELECT 'orders', id, 'created' FROM INSERTED - 扣库存:
UPDATE p SET stock = stock - i.qty FROM products p INNER JOIN INSERTED i ON p.id = i.product_id - 校验约束:
IF EXISTS (SELECT 1 FROM INSERTED i LEFT JOIN products p ON i.product_id = p.id WHERE p.stock - 注意:所有条件判断必须用集合式(
EXISTS、NOT EXISTS),不能写WHERE @qty > @stock
MySQL 触发器里写多条语句要改 DELIMITER
MySQL 默认把分号当语句结束符,直接写多条会报 ERROR 1064。必须临时改分隔符,否则语法通不过。
- 必须先执行
DELIMITER $$,再写CREATE TRIGGER ... BEGIN ... END$$ -
BEGIN和END必须成对,中间每条语句以分号结尾 - 触发器内禁止
COMMIT/ROLLBACK,任一语句失败整个事务回滚 - 示例:
DELIMITER $$ CREATE TRIGGER after_order_insert AFTER INSERT ON orders FOR EACH ROW BEGIN INSERT INTO order_logs VALUES (NEW.id, 'created'); UPDATE inventory SET stock = stock - NEW.qty WHERE id = NEW.item_id; END$$ DELIMITER ;
游标不是解法,是退路
游标能“保证逐行”,但代价极高:锁时间长、并发差、易死锁、难调试。除非真要调外部 API 或写异步任务,否则不值得碰。
- 游标必须显式
OPEN/CLOSE/DEALLOCATE,漏掉任一环节会句柄泄漏 -
@@FETCH_STATUS在并发下不稳定,建议加IF @@FETCH_STATUS 0 BREAK - 更合理的做法是:触发器里只做轻量集合操作,重逻辑(如发消息、调 HTTP)扔给队列表 + 外部服务消费
- SQL Server 2016+ 可用
STRING_AGG汇总,但仍是汇总,不是逐行替代方案
真正难的不是写游标或改分隔符,而是把“我要处理每一行”这个直觉,换成“我要处理这一批数据”。多数时候,集合操作写出来更短、跑得更快、线上更稳——只是需要跳出循环思维那一小步。

















