触发器内不可用事务控制语句,因其天然属于宿主事务;mysql before insert 中 new.id 为 null,after 才可用;update 中需用 null 安全比较判断字段是否真修改;触发器逐行执行且同步阻塞,性能与锁风险高,须真实数据压测。

触发器里不能用事务控制语句
MySQL 和 PostgreSQL 的触发器函数内部不允许显式使用 BEGIN TRANSACTION、COMMIT 或 ROLLBACK。这不是语法限制,而是逻辑冲突——触发器本身就在宿主事务中运行,它的所有操作天然属于该事务的原子单元。
常见错误现象:ERROR 1305 (42000): SAVEPOINT does not exist 或 PostgreSQL 报 ERROR: cannot begin/end transactions in PL/pgSQL,往往是因为在 BEFORE 触发器里写了 ROLLBACK TO SAVEPOINT 试图“局部回滚”。
- 真正需要拦截操作时,用
RAISE EXCEPTION(PostgreSQL)或SIGNAL SQLSTATE(MySQL 5.5+)主动报错,让整个事务失败 - 想实现条件跳过更新?改用
BEFORE UPDATE中修改NEW字段值,而不是靠事务控制 - PostgreSQL 中若依赖
SAVEPOINT做嵌套错误处理,必须把逻辑移到外部存储过程,而非触发器内
INSERT 触发器读不到自增 ID 的坑
在 MySQL 的 BEFORE INSERT 触发器里,NEW.id 还没被赋值(值为 NULL 或默认值),哪怕字段定义了 AUTO_INCREMENT;只有到了 AFTER INSERT,它才可用。
典型使用场景:插入用户后自动写入日志表,并记录新用户的 id。如果在 BEFORE 里就去查或拼接,会拿到空值或 0。
-
BEFORE INSERT:适合校验、补全字段(如设置created_at)、修改NEW值 -
AFTER INSERT:才能安全读取NEW.id,也才能做跨表插入、调用外部函数等副作用操作 - 注意 PostgreSQL 的
RETURNING子句虽能返回 ID,但触发器无法捕获它——RETURNING是客户端可见的,不是触发器执行上下文的一部分
UPDATE 触发器里判断字段是否真被修改
别直接比 OLD.col != NEW.col,NULL 参与比较永远返回 UNKNOWN,导致条件失效。这是最常被忽略的兼容性细节。
创想商务B2B网站管理系统(橙色风格版)V3.0 注意事项:该风格模板基于创想商务B2B网站管理系统(v3.0)使用。 部分特色功能如下: 1、一健在线安装 : 2、商铺独立二级域名: 3、阶梯价批发: 4、零售商城: 5、会员等级自由转换: 6、在线交易: 7、会员商家多方位推广: 8、多种赢利模式: 9、分类多属性关联: 10、自主风格模板设计: 11、HTML静态化处理: 12、灵活SEO
正确做法是用三值逻辑安全的判断方式:
- MySQL:用
IFNULL(OLD.col, '') != IFNULL(NEW.col, ''),或更稳妥的NOT (OLD.col NEW.col)(是 NULL 安全等于) - PostgreSQL:直接用
OLD.col IS DISTINCT FROM NEW.col,这是标准 SQL,专为 NULL 设计 - 如果只关心某几个字段变化(比如只在
status改变时发通知),务必把判断写进触发器体,而不是靠外部应用层过滤
触发器性能开销藏在隐式锁和重复执行里
每个触发器都是原 SQL 执行路径上的同步钩子,它不排队、不异步,且可能引发额外锁等待。特别是 AFTER 触发器调用的 INSERT/UPDATE,容易形成锁链。
一个真实案例:订单表 AFTER UPDATE 触发器往日志表写记录,而日志表恰好有 ON DELETE CASCADE 关联到另一个大表,结果单条更新卡住 2 秒以上。
- 避免在触发器里做远程调用、文件 I/O、复杂 JOIN 查询
- 批量更新(
UPDATE ... WHERE id IN (...))会为每一行都执行一次触发器,不是只执行一次——这点常被误判 - MySQL 8.0+ 支持触发器禁用(
SET SESSION sql_log_bin = 0),但仅限于当前会话,且需 SUPER 权限,线上慎用
触发器真正的复杂点不在语法,而在它像影子一样附着在数据变更上:你删一行,它可能悄悄改十行;你改一个字段,它可能触发三层嵌套更新。上线前一定得用真实数据量压测,光看单条语句的执行计划没用。









