
前几天项目里又有人来问我MySQL触发器TRIGGER到底该怎么写什么时候该用为什么网上教程一抄就报错答案又众说纷纭。我索性把这些年实际写触发器踩过的坑、拆过的案例、总结过的排查思路整理成一篇聊清楚它到底能干什么、怎么落地、以及哪些场景千万别碰。MySQL触发器本质上是一种特殊的数据库对象它绑定在某张表上当这张表发生INSERT、UPDATE、DELETE操作时数据库引擎会自动执行你预先定义好的一段SQL逻辑整个过程不需要应用层主动调用。它很适合处理“数据写入后必须跟着做点什么”的一类固定动作比如扣库存、记日志、做校验、同步汇总。这篇内容适合所有正在用MySQL的数据开发、后端工程师也包括准备面试时想系统理解触发器原理的同学我会尽量用实际可运行的案例把关键细节讲透。1. 触发器到底是什么理解它的运行机制和工作场景1.1 触发器的定义与核心机制MySQL官方文档里对触发器的定义很简洁触发器是与表关联的、在某个事件发生时自动激活的命名数据库对象。这里的“事件”指的就是针对某张表的DML操作INSERT、UPDATE、DELETE三种事件再加上执行时机BEFORE和AFTER两种组合出了六种触发器类型。用生活里的例子来理解最方便。你可以把触发器想象成厨房里的烟雾报警器你不用手动按它也不用在炒菜前“调用”它只要厨房里出现烟雾浓度超标这个事件报警器就会自动响应。数据库里的触发器也类似它默默挂在某张表上只要符合条件的SQL语句经过这张表引擎就会自动执行你写好的那一小段逻辑。有一个关键点你刚接触时容易忽略触发器不是在应用里通过代码显式调用的而是由MySQL引擎在语句执行阶段自动判断并激活。应用端可能同时有几十个服务连到同一个库它们都只是在执行普通的DML语句根本不知道自己会触发什么额外的动作。这就带来一个明显的优点被强制固化在数据库层不会因为应用漏写一段逻辑而导致数据不一致。但它也是缺点来源因为它像隐形逻辑一样排查问题时如果不主动查看触发器列表很容易像见鬼一样找不到数据变化的原因。1.2 什么时候该用触发器典型业务场景以我实际做过的项目为参照下面这些场景用触发器非常合适。第一库存扣减和订单状态联动。订单表插入一条新记录后必须同步扣减商品表的库存。这类动作业务上几乎百分之百要发生而且一旦漏掉就可能导致超卖。放在应用里做也行但需要每个写订单的服务都记得调库存服务。用AFTER INSERT触发器把扣库存动作挂在订单表上至少能保证“订单插入成功但库存没扣”这件事不会发生。第二审计日志。核心账号表、余额表发生变化时希望自动记录“旧值是什么、新值是什么、什么时候变的”。这类审计要求非常强调完整性用AFTER UPDATE或AFTER DELETE触发器把新旧值捞出来写进日志表比应用层逐个字段比对要靠谱得多。第三数据校验。有些格式约束用普通CHECK约束不好写或者希望报错信息更加友好。在BEFORE INSERT或BEFORE UPDATE触发器里做校验一旦不满足条件就通过SIGNAL语句主动抛错阻止整条SQL正常执行。比如手机号格式校验、金额必须大于0、删除受保护记录之前先拦截。第四数据汇总与冗余字段维护。比如用户表插入新用户后自动往统计表里增加用户总数或者更新订单总金额时自动同步到客户表的“累计消费”字段。这种场景适合低频、小数据量的同步如果在高频大表上做就得慎重后文我会展开讲性能问题。1.3 触发器的六种类型与各自用途MySQL规定触发器只能针对INSERT、UPDATE、DELETE三种DML事件再搭配BEFORE和AFTER两个时机总共六种组合。下面这张表是我根据实践整理的对照触发器类型事件时机典型用途BEFORE INSERT插入前数据校验、字段默认值处理、主键生成前干预AFTER INSERT插入后扣减库存、写日志、同步汇总统计BEFORE UPDATE更新前校验新值是否合法、统一补上updated_at时间戳AFTER UPDATE更新后记录变更日志、同步其他表的冗余字段BEFORE DELETE删除前删除保护、把被删数据归档到回收站表AFTER DELETE删除后清理关联表、扣减统计计数、记录删除日志这里有一个版本相关的细节值得注意。在MySQL 5.7.2之前同一张表同一个事件时机最多只能创建一个触发器。比如orders表上只能有一个AFTER INSERT触发器你再想建第二个直接报错。从5.7.2开始这个限制取消了同一个事件时机可以创建多个触发器并且可以显式指定它们的执行次序。这个改动在实际项目里非常实用你可以把“扣库存”“记日志”“更新统计”拆成三个独立触发器而不是把一大堆逻辑堆进一个巨型触发器里维护和理解都舒服得多。2. 创建触发器的完整语法与实操案例2.1 CREATE TRIGGER语法逐行拆解创建触发器的标准语法长这样CREATE TRIGGER trigger_name {BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON table_name FOR EACH ROW trigger_body;逐项拆开来看。trigger_name是触发器名称最好带环境标识和业务前缀比如trg_orders_after_insert一眼能看出它是哪张表、在什么时机触发、干什么用的。接着是触发时机和事件BEFORE表示在SQL真正执行前激活AFTER表示在SQL执行完成后激活。ON table_name表明这个触发器挂在哪张表上它只能建在基表上不能建在临时表或视图上。FOR EACH ROW表示这是行级触发器这是MySQL唯一支持的粒度它会对当前语句影响的每一行执行trigger_body。重点说trigger_body。如果逻辑只有一句话可以直接写但大多数真实场景都是一段复合语句这时候必须用BEGIN...END包裹。由于trigger_body里允许出现分号而我们平时用的MySQL命令行客户端自己就用分号作为语句结束符为了让MySQL正确识别触发器定义的边界创建触发器之前通常要修改默认的语句定界符也就是大家常说的delimiter。例子如下DELIMITER $$ CREATE TRIGGER trg_orders_after_insert AFTER INSERT ON orders FOR EACH ROW BEGIN UPDATE products SET stock stock - NEW.quantity WHERE product_id NEW.product_id; END$$ DELIMITER ;如果你漏了DELIMITER操作在命令行工具里执行会频繁遇到语法错误提示因为客户端在分号处就把CREATE TRIGGER语句截断了。用Navicat、DataGrip这类图形工具时工具通常会自动处理定界符但在命令行操作时必须养成写DELIMITER的习惯。2.2 案例1订单表写入后自动扣减库存先创建订单表和商品表作为案例基础CREATE TABLE products ( product_id INT PRIMARY KEY, stock INT NOT NULL ); CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL, quantity INT NOT NULL );现在业务要求每次orders表成功插入一条订单products表对应商品的库存就必须扣掉对应的quantity。这个动作用AFTER INSERT触发器做最直接。为什么用AFTER而不是BEFORE因为订单还在插入过程中你并不需要提前操作库存等订单真正写入成功后再扣库存可以避免订单插入失败时库存被白白扣掉的情况。如果库存扣减失败整个事务会回滚订单插入也会失败数据一致性由事务保证。完整写法如下DELIMITER $$ CREATE TRIGGER trg_orders_after_insert AFTER INSERT ON orders FOR EACH ROW BEGIN UPDATE products SET stock stock - NEW.quantity WHERE product_id NEW.product_id; END$$ DELIMITER ;这里最有意思的是NEW这个关键字。NEW代表当前正在插入的那行数据NEW.quantity就是刚才插入的新订单数量。对INSERT触发器来说NEW是只读还是可写在BEFORE INSERT里你可以通过SET NEW.xxx 值来修改将要写入的数据但在AFTER INSERT里数据已经写入刚完成你再修改NEW已经没有意义了所以实践中AFTER触发器里我不会尝试去改NEW字段。这个案例看起来很简单但实际生产环境有个并发隐患。上面直接通过UPDATE products SET stock stock - NEW.quantity来扣库存在高并发下存在一定的超卖风险除非你有适当的锁机制兜底。如果你们项目对库存准确性要求很高在触发器里可以通过SELECT ... FOR UPDATE锁定商品行并且先检查库存是否充足。但更稳妥的做法是把扣库存作为一个条件更新来看待UPDATE products SET stock stock - NEW.quantity WHERE product_id NEW.product_id AND stock NEW.quantity;如果影响行数为0说明库存不足此时触发器要主动抛错阻止订单插入比如SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 库存不足无法下单;2.3 案例2更新余额时自动记录变更日志审计类需求天然适合触发器。假设account表如下CREATE TABLE accounts ( id INT PRIMARY KEY, name VARCHAR(50), balance DECIMAL(10, 2) );希望每次balance发生变化时都往日志表里写入一条记录包含账号id、旧余额、新余额和变更时间CREATE TABLE account_balance_log ( id INT AUTO_INCREMENT PRIMARY KEY, account_id INT NOT NULL, old_balance DECIMAL(10, 2), new_balance DECIMAL(10, 2), change_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这个场景应该用AFTER UPDATE触发器因为日志要记录的是“已经发生”的变更而不是“即将发生”的变更AFTER时机能拿到最新的真实结果。触发器写法如下DELIMITER $$ CREATE TRIGGER trg_account_after_update AFTER UPDATE ON accounts FOR EACH ROW BEGIN INSERT INTO account_balance_log(account_id, old_balance, new_balance) VALUES (OLD.id, OLD.balance, NEW.balance); END$$ DELIMITER ;注意这里OLD和NEW同时出现。OLD取的是更新前的旧行NEW取的是更新后的新行。哪怕一条UPDATE语句同时更新了users表里的一千行这个触发器也会执行一千次每次OLD和NEW都指向各不相同的那一行数据这就是FOR EACH ROW的含义。实际测试时我发现一个容易忽略的点如果应用层每次都把整行所有字段都UPDATE一遍即使balance值没有变化AFTER UPDATE触发器依然会被执行日志里照样会出现一堆没意义的“变更”。如果你只关心余额真正变化时的记录需要在触发器里加一个判断IF OLD.balance NEW.balance THEN INSERT INTO account_balance_log(account_id, old_balance, new_balance) VALUES (OLD.id, OLD.balance, NEW.balance); END IF;这种“值没变也触发”的行为对很多新手来说是第一个隐形坑。2.4 案例3删除主表数据前做删除保护与归档再来看一个BEFORE DELETE触发器的实战用法。我们先给users表加一个is_protected字段用于标记受保护账号CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(50), is_protected TINYINT DEFAULT 0 );如果直接执行DELETE删除受保护用户业务风险很高。用BEFORE DELETE触发器做拦截是一个常见的兜底方案DELIMITER $$ CREATE TRIGGER trg_users_before_delete BEFORE DELETE ON users FOR EACH ROW BEGIN IF OLD.is_protected 1 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 受保护用户不允许删除; END IF; END$$ DELIMITER ;SIGNAL的作用是主动抛出异常SQLSTATE 45000是用户自定义异常的标准状态码后面的MESSAGE_TEXT会作为异常信息返回给客户端。系统自动抛错时整条DELETE语句会被回滚一张数据都不会删掉。另一种典型场景是“删除即归档”。有些业务希望删除用户不是物理删除而是先把整行数据迁到archive表里再执行真正的DELETE。这个操作可以放在BEFORE DELETE里完成DELIMITER $$ CREATE TRIGGER trg_users_before_delete_archive BEFORE DELETE ON users FOR EACH ROW BEGIN INSERT INTO users_archive(id, name, is_protected, deleted_at) VALUES (OLD.id, OLD.name, OLD.is_protected, NOW()); END$$ DELIMITER ;这样用户被删除后原表数据消失了但归档表里留了一份完整快照。归档操作放在BEFORE时机有一个好处万一归档插入失败删除本身也会中止避免数据真的消失后归档却没跟上。2.5 案例4用BEFORE UPDATE统一维护更新时间字段很多设计里users表需要维护一个updated_at字段。常见做法是建表时用DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这也是一种隐式处理机制。但如果你还需要在更新前做其他业务判断用触发器写会更加直观DELIMITER $$ CREATE TRIGGER trg_users_before_update BEFORE UPDATE ON users FOR EACH ROW BEGIN SET NEW.updated_at NOW(); END$$ DELIMITER ;在BEFORE UPDATE触发器里NEW是可以修改的你通过SET NEW.updated_at NOW()把所有“即将写入”的数据行都补上当前时间。这个改动会真实生效在最终写入的数据上。掌握这个特性之后你完全可以在BEFORE触发器里做字段级别的新值修正比如把空字符串统一置为NULL把金额统一向上取整等等。3. 触发器的运行机制与NEW/OLD深入理解3.1 NEW与OLD伪行的取值规则触发器操作的数据不是直接用变量名访问的而是通过NEW和OLD这两个“伪行”来获取。它们不是真正的表而是代表当前操作涉及的那一行数据的前后快照。具体规则如下INSERT触发器只有NEW可用代表即将插入或刚插入的新行。DELETE触发器只有OLD可用代表即将删除或刚删除的旧行。UPDATE触发器NEW和OLD都可用OLD是更新前的旧值NEW是更新后的新值。我再补充一个容易忽略的细节在BEFORE INSERT和BEFORE UPDATE中你可以修改NEW的字段值通过SET NEW.column value来干预最终写入的数据这本质上是给了数据库逻辑一个“二次加工”的入口。但在AFTER触发器中数据已经落库任何对NEW的赋值都不会影响真实表数据虽然某些版本下语法上不一定报错但它办不到任何事所以不要写这种没有意义的代码。OLD字段永远是只读的因为已经发生过的历史值不能被触发器改变。需要注意NEW和OLD只能读取当前触发表的字段你没法从中拿到另一张表的列。如果想拿商品表的库存数量必须用SELECT查询。3.2 触发器的执行顺序与事务边界一条DML语句在MySQL内部大致经历这样的阶段检查数据是否合法、执行BEFORE触发器、执行真正的DML操作、执行AFTER触发器。如果过程中出现错误整个语句及对应的触发动作都会被回滚。这里有一个容易绕晕的点多个同类型同一个事件时机的触发器如何排序。5.7.2之后同一时刻允许多个触发器存在它们的默认执行顺序是按创建时间先后执行的。如果你需要精确控制先后关系可以在创建新触发器时通过FOLLOWS和PRECEDES指定它跟在哪个触发器之后执行或者在某一个触发器之前执行。比如CREATE TRIGGER trg_orders_after_insert_2 AFTER INSERT ON orders FOLLOWS trg_orders_after_insert FOR EACH ROW BEGIN -- 第二个触发器的逻辑 END;这意味着trigger的执行顺序可以被管理而不是永远靠“感觉”在猜。关于事务边界触发器和它的触发语句共享同一个事务。比如你执行一行UPDATE accounts语句该语句如果没有显式开启事务默认也是自动提交但这不影响触发器在同一个原子单元里运行任何触发器内的异常都会让整个DML语句回滚。尤其是AFTER触发器虽然表数据已经写入但SIGNAL一抛错已写入的操作照样会被撤销。所以有人以为AFTER触发器里抛错就来不及了其实是误解。另外要注意MySQL触发器中不允许出现显式事务控制语句比如START TRANSACTION、COMMIT、ROLLBACK。因为触发器本身就是一个事务流程的一部分再嵌套事务控制没有意义也容易出问题。3.3 触发器与存储过程、事件调度的对比很多人分不清触发器、存储过程、事件调度器EVENT这三种东西我列个表简单对照一下功能项触发器 TRIGGER存储过程 PROCEDURE事件调度器 EVENT触发方式表的DML事件自动触发显式CALL调用按时间计划自动执行能复用吗一般不显式调用可以被多处调用不关注复用适合场景数据变更后的固定动作复杂业务逻辑封装定时统计数据、定期清理事务控制不能显式控制事务可以控制事务可以控制事务参数支持不支持参数使用NEW/OLD支持输入输出参数不支持参数触发器最大的价值在于“自动”二字它不依赖外部调用和定时器但也正因为如此它的逻辑对开发者来说是隐性的。系统运行时间久了新人接手时根本不知道某张表数据为什么变化这是触发器被很多人排斥的原因。存储过程则更透明一点至少你能通过应用代码里显式的CALL语句看到调用点但它的缺点是需要主动调用做不到“自动”。事件调度器则是数据库层面的定时任务比如每天凌晨清理过期数据它和表上的DML事件没关系。所以选型上我自己的习惯是跟行级数据变更强绑定的、必须保证一致性的动作优先考虑触发器复杂的多步骤业务处理写成存储过程并由应用显式调用纯定时统计类的任务用事件调度器。4. 触发器管理查看、删除、修改与权限控制4.1 查看触发器列表与定义项目跑了一段时间后你想知道某张表上到底挂了多少触发器最简单的命令是SHOW TRIGGERS;这条命令会把当前数据库里的全部触发器列出来内容包括触发器名称、事件、关联表、SQL语句片段等。如果只想看某张表相关的使用LIKE匹配触发器名或直接模糊匹配表名SHOW TRIGGERS LIKE trg_orders%;如果需要更完整的元数据包括定义者的权限信息、创建时间等可以直接查系统表information_schema.TRIGGERSSELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA your_db_name;用这个方式排查“某张表的数据为啥突然变了”很管用先查有没有触发器再查ACTION_STATEMENT具体做了什么大部分数据诡异变化的问题都能一眼找到元凶。4.2 删除与重建触发器的正确姿势删除触发器比建表删表简单DROP TRIGGER IF EXISTS trg_orders_after_insert;删的时候要特别注意如果你执行DROP TABLE删除某张表该表上所有的触发器也会一并被删除而且不会给你任何提示。如果你用mysqldump只备份表结构默认情况下触发器是否包含在主备份中取决于你导出时的选项后面我会专门讲。MySQL没有像MariaDB那样提供一个直接禁用触发器的开关。你需要临时停用某个触发器时只能先DROP用完之后再CREATE回来。如果这种临时停用操作很频繁建议把每个触发器的DDL语句单独存成SQL脚本文件统一放在数据库变更目录里需要重建时直接执行脚本。我自己管理时还会在触发器名称里带上版本号或业务标识这样DROP之后重建时不容易混淆。一个进阶的“软禁用”技巧是使用会话变量或用户变量做开关。比如在触发器内部先判断IF disable_trigger IS NULL THEN -- 执行正常逻辑 END IF;需要禁用时业务端先执行SET disable_trigger 1触发器就会自动跳过核心逻辑。对临时性的维护操作这个方案可以省去频繁删建触发器的麻烦。但要注意这个变量只在当前会话内有效其他连接仍然会触发完整逻辑所以它不能作为永久的启用/禁用开关。4.3 触发器与数据库备份、迁移的细节备份迁移时触发器是很容易被忽略的一块。mysqldump导出单库时如果指定了--triggers选项并且版本支持它会连同触发器一起导出默认情况下对于触发器的处理逻辑有时候会因参数变化而改变。稳妥的做法是备份完成后专门用下面这条命令单独导出触发器避免遗漏mysqldump -u用户名 -p 库名 --triggers --no-create-info --no-data triggers_backup.sql更严格的团队流程会把触发器脚本作为数据库迁移版本的一部分由专人统一管理而不是完全依赖mysqldump。恢复触发器时还要留意DEFINER权限问题。如果触发器里的DEFINER指定的用户在新环境不存在或者权限不足导入时会报错恢复后也可能出现触发器执行失败。此时要么提前创建同名用户要么在导入前用sed等工具把DEFINER替换成新环境的数据库账号。主从复制环境更要多做一步验证在从库上手工创建一张测试表建一个测试触发器确认从库复制链路不会因为触发器产生异常。因为有些场景下主库的触发器动作在从库重放时行为并不一致尤其是使用了存储函数或依赖特定参数设置时容易出现复制中断。5. 触发器实战中的常见问题与排查技巧5.1 触发器不生效的几类原因有段时间我排查过一个“触发器好像没生效”的问题最后发现是另一个同类型的触发器先执行并且提前SIGNAL抛错导致后续逻辑根本走不到。所以遇到不生效我建议按顺序排查以下原因触发器是否真的存在用SHOW TRIGGERS查看别光靠记忆。是不是同表同事件有多个触发器执行顺序和后建的那个预期不一致检查事件表和触发器定义中的库名是否匹配有时候你在demo库建了触发器却在test库执行DML那当然不触发。触发器逻辑里是不是有IF条件被绕过比如余额日志触发器只在OLD.balance NEW.balance时写入但你更新时两次值相同日志就是空的这属于设计符合预期。有没有调用SIGNAL导致整个触发器在早期位置退出这是隐蔽原因。另外DML语句对触发器的影响也有区别。使用TRUNCATE TABLE清空数据时触发器不会触发因为TRUNCATE被MySQL归类为DDL操作不是DELETE。这也是一个经典的“为什么触发器没跑”的原因。5.2 递归触发与死锁如何避免触发器里最常见的“事故”就是递归。最直接的禁止项MySQL不允许触发器去修改自己所在的表。比如你在orders表的AFTER INSERT触发器里再执行一条INSERT INTO ordersMySQL会直接报错Cant update table orders in stored function/trigger because it is already used by statement which invoked this stored function/trigger。但更隐蔽的是不同表之间的交叉触发。比如orders表的AFTER INSERT触发器里UPDATE了products表而products表又有AFTER UPDATE触发器来UPDATE orders表两个触发器就可能形成循环调用。MySQL会检测部分递归并报错但一些复杂链路在死锁超时前不一定能及时暴露。我处理这种问题时有一条经验不要用触发器去维护另一张“最终都会被业务大量更新”的核心表之间的交叉逻辑。如果A表触发器要更新B表B表上不能再建触发器反过头来更新A表或C表除非有严格的层级划分。尽量让触发器只做“单向、叶子节点”的更新比如A表的新增动作更新一张统计表C统计表C不再有触发器去更新A或B。如果实在需要防止重复触发可以给表加一个标志字段或利用user_variable做引用标记但这属于比较绕的方案能不用尽量不用。5.3 触发器影响性能的真相和优化建议触发器对性能的影响经常被人低估。FOR EACH ROW是行级触发一条UPDATE语句更新一万行触发器就会被执行一万次。如果触发器内部还包含客户余额表的历史日志、汇总统计等操作那么单条SQL的实际开销会翻好多倍。我第一次踩到性能坑是给一张订单表加了AFTER INSERT触发器每次插入都更新商品表并写入日志表。单条插入数据时很快但一次批量导入十几万条订单时导入时间从原来的几分钟直接拉长到一个多小时而且因为触发器执行过程中持有行锁并发导入时大量出现了锁等待和超时监控图上锁等待曲线触目惊心。从那以后我对触发器性能优化有了几条固定的检查项触发器内部涉及的每张表WHERE条件列务必有合适索引。比如UPDATE products WHERE product_id NEW.product_id如果product_id没索引每次触发器执行都是全表扫描批量导入场景下就是灾难。触发器只做必要且轻量级的操作。日志表要避免在触发器内部做复杂聚合查询更不要在触发器里调用外部存储过程去处理大量数据。大批量数据变更时先评估是否真的需要逐行触发。如果只是同步汇总数据完全可以用存储过程在批处理结束后统一执行一次UPDATE替代逐行触发器。触发器内不要使用动态SQL、不要执行DDL这类操作会严重拉长锁持有时长增加死锁风险。另外在MySQL默认的RR隔离级别下触发器执行过程中的读操作会使用一致性读但写操作会加行锁直到事务结束。如果触发器内部执行了一个慢查询那么这条DML语句会一直持锁其他事务对同一行数据的更新都会排队。热词里提到的“锁表”“锁等待”问题很多都是触发器里藏着慢SQL造成的排查时可以优先看看当前运行的事务和锁等待事件。5.4 触发器踩坑实录三个值得说的真实案例第一个案例是误把AFTER时机用成了“校验”。我在一个用户表上建了AFTER INSERT触发器想在插入后校验某个字段是否符合业务规则不符合就SIGNAL报错。虽然AFTER里报错确实会让整个事务回滚表面上看似可行但有一个问题AFTER触发器是在数据落库之后才执行的如果表比较大回滚成本和造成锁等待的时间都要更长。所以涉及校验、拦截、阻止写入的逻辑优先用BEFORE触发器让错误在数据写入之前暴露。第二个案例是触发器里做审计日志时忘记考虑批量更新。某次运营人员执行了一条不带条件的UPDATE语句意图是更新一个字段结果把全表数据都改了。虽然AFTER UPDATE触发器把每一行的OLD和NEW都记录到了日志表但日志表瞬间多了几十万行而且因为触发器需要逐行读取和写入那条UPDATE语句跑了很久期间其他业务全部卡在锁等待上。后来我们给所有账号更新类操作都增加了WHERE条件规范要求同时也把批量更新操作改成“先SELECT主键再分批UPDATE”的方式。第三个案例是主从环境下的触发器问题。某次我们在主库建了一个触发器内部逻辑依赖一个自定义函数直接调用后发现从库复制中断。排查原因后发现主库执行DML时触发器的动作在主库执行但部分版本的binlog设置会导致从库重放时对存储函数或触发器的执行要求更高需要提前设置log_bin_trust_function_creators参数否则创建函数或复制过程中就会报错。这个案例给我的教训是涉及触发器、函数和主从复制的组合场景必须在搭建测试环境时完整演练一遍不要等到生产复制中断才去补救。5.5 什么时候不要用触发器应用层和数据库层怎么划分触发器很好用但它不是万能的。我建议在下面几类场景里避免使用触发器核心交易链路中对响应时间极其敏感的操作。触发器给每条DML增加额外耗时即使只有几毫秒高频场景下累积影响也很大。需要与外部系统交互的场景。比如订单插入后需要调HTTP接口通知配送系统这种动作不应该放在触发器里因为数据库事务期间的网络调用既不可控也不安全。复杂业务规则判断。规则判断中间依赖大量历史数据、缓存、其他服务的结果把这种逻辑塞进触发器会让数据库变得臃肿且极难调试。临时的、实验性质的逻辑。如果只是跑一次数据修复用完就删的逻辑不要用触发器固化下来尽量用一次性的SQL脚本。那么触发器适合放在哪一层我的判断标准是它是单纯的数据一致性保障动作不依赖网络IO不依赖不可控的外部服务而且执行时间很短。满足这些条件用触发器是合适的。比如扣库存、记审计日志、补时间戳、做归档这些都属于典型的“数据层内部固定动作”放在数据库里自动执行反而是最省心的方式。我个人的一点体会是触发器最怕的不是触发器本身而是团队对它缺乏管理。你只要把触发器脚本纳入版本管理、命名规范清晰、每次变更都经过review并且定期用SHOW TRIGGERS清理掉没用的触发器它完全可以成为数据一致性体系里一个可靠的守门员。尤其是那种“业务上无论如何都不能漏做”的固定动作交给触发器以后我睡起觉来都踏实不少。如果你把触发器当成一个隐藏的定时任务来用在设计时没有想清楚它的触发边界和性能影响那大概率会在某次大促、某个深夜跑批的时候收到报警电话。用好它、管好它、控制它的作用范围这个工具会让你的数据库省掉大量琐碎的手工维护工作。