触发器不是行级循环器,而是集合操作钩子;SQL Server 的 inserted 是表非标量,MySQL 的 NEW 是单行别名;误用导致“漏处理”,本质是逻辑错配而非功能缺陷。

不是“只处理了部分数据”,而是你误把语句级触发器当成了行级循环器——SQL Server 和 PostgreSQL 的 AFTER UPDATE 触发器本就只执行一次,inserted 里装着全部新值;MySQL 的 FOR EACH ROW 才真逐行触发,但容易因内部逻辑写错导致看似“漏处理”。问题核心不在数据库,而在你怎么读 inserted 或用 NEW。
SQL Server 中 SELECT @id = id FROM inserted 只取一行
这是最典型的误用:把集合当单值用。SQL Server 的 inserted 是一张内存临时表,不是标量变量。
- ❌ 错误写法:
DECLARE @id INT; SELECT @id = id FROM inserted;——@id最终只存其中某一行的值(SQL Server 不保证顺序,也不报错) - ✅ 正确做法:用集合操作,比如
INSERT INTO audit_log (id, op) SELECT id, 'UPDATE' FROM inserted; - ⚠️ 如果必须逐行分支逻辑(如 status=1 调 A、status=2 调 B),先用
SELECT ... INTO #tmp分离数据,再分别处理,别在触发器里直接EXEC循环调用
MySQL 触发器里用 NEW.id 写条件判断会丢数据
MySQL 的 BEFORE UPDATE 触发器确实是行级的,但 NEW 和 OLD 是单行别名,不支持 WHERE、JOIN 或子查询——它只代表当前正在被处理的那一行。批量更新时,触发器体被反复执行,但每次看到的 NEW 都是孤立的。
- ❌ 错误写法:
IF NEW.status = 1 THEN INSERT INTO log_table VALUES (NEW.id); END IF;—— 这段逻辑本身没错,但如果里面加了SELECT COUNT(*) FROM config或UPDATE other_table SET x = NEW.id,就会因锁、全表扫描或触发器嵌套限制失败 - ✅ 安全写法:只做确定性字段赋值,如
SET NEW.updated_at = NOW();;所有多行关联逻辑移出触发器,改用应用层或日志表+异步消费 - ⚠️ 特别注意:
REPLACE INTO和INSERT ... ON DUPLICATE KEY UPDATE在 MySQL 8.0.19 前不触发BEFORE INSERT,你以为的“插入校验”根本没跑
分批 UPDATE 也没绕过触发器的逐行开销
很多人以为用 LIMIT 1000 分批就能缓解性能压力,但对 MySQL 来说,这只是把 10 万次触发器调用拆成 100 批 × 1000 次——上下文切换、函数调用(如 NOW())、锁竞争反而更重。
- ❌ 无效优化:加索引、缓存子查询结果、拆 IF 分支——瓶颈不在 SQL 写法,而在“每行必进触发器”这个机制本身
- ✅ 真实解法:禁用触发器,改用生成列(
updated_at DATETIME AS (NOW()) STORED)、应用层显式传参(created_by)、BINLOG 解析异步统计 - ⚠️ 如果审计强要求保留触发器,底线是:只允许
BEFORE类型,体内禁止SELECT/INSERT/UPDATE,禁止自定义函数,仅用确定性表达式赋值
真正容易被忽略的是:触发器不是业务逻辑容器,它是轻量钩子。一旦你开始在里面做 JOIN、查配置、调外部状态,就已经走在不可维护的路上了——不是它“只处理部分数据”,是你让它本不该干的事,干得越来越慢、越来越不可控。










