触发器中需从inserted/deleted表显式获取主键值,单行用select top 1,批量用join;复合主键须全字段匹配;注意事务回滚、权限、空表防护及性能优化。

触发器里怎么拿到业务主键值
触发器执行时没有“当前业务主键”这个概念,得从 INSERTED 或 DELETED 临时表里手动取。比如主表叫 orders、主键是 order_id,那必须显式写 SELECT order_id FROM INSERTED,不能假设它自动可用。
常见错误是直接在触发器里写 WHERE id = @id 却没声明或赋值 @id——SQL Server 触发器不传参,所有数据都得从 INSERTED/DELETED 查。
- 单行插入:用
SELECT TOP 1 order_id FROM INSERTED最稳妥(避免隐式多行报错) - 批量插入:必须用
JOIN方式关联,例如UPDATE ext SET ext.status = 'pending' FROM order_ext ext INNER JOIN INSERTED i ON ext.order_id = i.order_id - 如果主键是复合键(如
(tenant_id, order_no)),JOIN条件必须全字段匹配,漏一个就关联失败
扩展属性表更新失败但主表已提交
默认 AFTER INSERT 触发器出错会导致整个事务回滚——这看似安全,但实际常被忽略:如果扩展表结构变更(比如新增了 NOT NULL 字段但没给默认值),触发器就会因插入失败而拖垮主表写入。
更隐蔽的问题是权限:触发器以执行语句的用户身份运行,但该用户可能对 order_ext 表只有 SELECT 权限,缺 UPDATE 或 INSERT 就静默失败(取决于设置)。
- 务必在触发器开头加
IF NOT EXISTS (SELECT 1 FROM INSERTED) RETURN,防空触发 - 所有
UPDATE/INSERT操作后,用IF @@ROWCOUNT = 0检查是否真改到了数据 - 测试时故意删掉扩展表某个字段的
INSERT权限,看主表操作是否真的被拦住——别只信日志
MySQL 8.0 用 BEFORE INSERT 做自动填充更稳
MySQL 不支持像 SQL Server 那样在 AFTER 触发器里改刚插入的行,但 BEFORE INSERT 可以直接改 NEW 对象,天然规避“主表已落盘、扩展表失败”的一致性风险。
不过要注意:如果扩展属性依赖主表自增 ID(比如 order_id 是 AUTO_INCREMENT),BEFORE 阶段 NEW.order_id 还是 NULL,必须等 AFTER 才能读到——这时候就得拆成两个触发器,或改用存储过程封装。
-
BEFORE INSERT适合填固定值(如NEW.created_by = USER())、计算字段(如NEW.amount_tax = NEW.amount * 0.1) - 涉及跨表查(如根据
customer_id查等级来设priority),必须用AFTER+ 显式UPDATE - MySQL 8.0.16+ 支持
INSERT ... ON DUPLICATE KEY UPDATE,但触发器不会触发ON DUPLICATE分支,这点容易误判
触发器性能卡在 JOIN 大表上
很多实现会写类似 UPDATE ext SET ext.flag = (SELECT config.value FROM config WHERE config.key = 'default_flag'),看着简单,但每次插入都查一次 config 表——如果 config 有 10 万行且没索引,单次触发器就拖慢 200ms。
更糟的是,在 INSERTED 上做子查询(如 SELECT (SELECT name FROM users u WHERE u.id = i.user_id) FROM INSERTED i),SQL Server 会为每一行重复执行子查询,而不是先 JOIN 后算。
- 把常量配置提前查出来存变量:
DECLARE @default_flag VARCHAR(10) = (SELECT value FROM config WHERE key = 'default_flag') - 关联用户/部门等维表,一律用
INNER JOIN INSERTED i ON u.id = i.user_id,别用相关子查询 - 触发器里禁止出现
EXEC sp_executesql或调用标量函数——它们无法内联,开销翻倍
真正难的不是写触发器,是想清楚“谁负责生成这个值”。如果扩展属性其实由上游服务决定,硬塞进触发器只会让逻辑散落、调试困难。留个字段先空着,比填错后再跑补偿脚本强。










