刚接手一个老项目时,我被一张订单表上的三四个触发器搞得头大:有的在更新库存,有的在写日志,还有个在同步汇总表。表面看业务逻辑都封装在数据库里,可一旦出现数据不一致,排查链路长到怀疑人生。后来我系统地梳理了一遍 MySQL 触发器(TRIGGER)的用法和边界,才慢慢摸清这东西到底该怎么用、什么时候别硬用。这篇就把我的实战经验完整拆开来讲,覆盖语法细节、真实业务场景、调试手段、迁移导出,以及那些文档里不会写的坑。
1. 触发器到底是干什么的:先理清需求再动手
触发器本质上是一种“自动化回调”:你告诉 MySQL,当某张表发生 INSERT、UPDATE 或 DELETE 时,自动去执行一段预先写好的 SQL 逻辑。它不需要应用层显式调用,只要数据库事件发生,就会在事务内部被触发。
1.1 触发器的典型使用场景
我实际项目里用得最多的是下面几类,你可以对照自己的业务判断是否适用。
第一类是数据一致性维护。比如订单表新增一条记录时,自动扣减商品库存;或者更新订单状态时,自动把关联表的更新时间戳刷新。这类操作如果放在应用层,需要写两三条 SQL 再包一层事务,一旦中途抛异常,很容易出现“订单建了但库存没扣”的脏数据。触发器把多个表的变更收敛进同一个事务,要么全部成功,要么全部回滚,从根上避免了不一致。
第二类是审计日志。某些核心表(比如用户表、资金流水表)需要记录每一次变更的操作人、变更时间和前后值。用触发器写审计日志,好处是无论数据从哪个入口进来——后台管理界面、定时任务、数据修复脚本,甚至 DBA 手动执行的 UPDATE——都会被完整记录,不漏一条。
第三类是冗余数据的同步维护。比如一张统计汇总表,每次有明细插入或更新时,自动重新计算汇总字段。这种需求在报表系统里很常见,用触发器可以让汇总数据“实时”更新,省去定时任务的延迟。
1.2 什么时候不应该用触发器
这是我在踩过坑之后最想强调的一点。触发器不是万能的,下面两种情况我强烈建议你别用。
第一种是核心业务规则的强依赖场景。比如订单金额计算、优惠券核销这类逻辑,一旦写进触发器,问题排查会非常痛苦。原因很简单:触发器是隐式的,应用层开发者在代码里完全看不到这段逻辑,新人接手时根本不知道还有这层“隐形业务”。等到线上数据出错,DBA 查了三天才发现是触发器里的一个除零错误。
第二种是高频写入的大表。触发器是行级触发的,也就是说,一条 UPDATE 语句批量更新 1 万行,触发器就被执行 1 万次。如果触发器里再访问其他表、做聚合计算,性能会成倍下降,甚至把数据库拖垮。这种场景下,把同步逻辑放到应用层或者异步队列里,才是更合理的选择。
触发器的本质是“让数据库自己管自己”,但它只能保证数据层面的约束,无法承载复杂业务。业务规则尽量留在应用层,触发器只负责数据完整性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 触发器的语法与执行细节:从建表到第一个触发器
很多人第一次写触发器,死在了分号上。我先给你一个完整的基线版本,再逐步拆解每个细节。
2.1 基础语法和权限要求
先看一段完整可运行的创建语句:
sql复制DELIMITER //
CREATE TRIGGER trg_order_before_insert
BEFORE INSERT ON t_order
FOR EACH ROW
BEGIN
SET NEW.create_time = NOW();
SET NEW.order_status = 1;
END //
DELIMITER ;
这里有几个关键点:
DELIMITER //的作用是告诉 MySQL 客户端,暂时把语句分隔符从分号改成双斜杠。因为触发器体内有多条 SQL,每条都以分号结尾,如果不改分隔符,MySQL 会在第一个分号处误以为命令结束了。CREATE TRIGGER需要TRIGGER权限。如果你的账号只有增删改查权限,需要在 MySQL 里单独授权:
sql复制GRANT TRIGGER ON your_database.* TO 'your_user'@'localhost';
FLUSH PRIVILEGES;
还有一种情况要注意:MySQL 8.0 之后,触发器创建时会检查使用者的权限。如果你用的是 root 账号,没问题;但如果是普通账号,而触发器里访问了其他表,执行时可能会因为权限不足而报错。所以生产环境里建议触发器使用专用账号创建,避免权限混乱。
2.2 触发时机与 FOR EACH ROW 的理解
触发器语法里必填两件事:触发时机和触发事件。
触发时机只有两个:BEFORE 和 AFTER,分别代表在 SQL 语句执行前和执行后触发。触发事件有三种:INSERT、UPDATE、DELETE。组合起来一共 6 种触发方式,你也可以在一张表上创建多个触发器分别处理不同事件。
FOR EACH ROW 表示这是一个行级触发器。MySQL 不支持语句级触发器(SQL Server 和 Oracle 支持),所以每一行数据受影响,触发器就会执行一次。这在批量操作时是性能分水岭,后面性能章节会单独讲。
2.3 NEW 和 OLD:触发器里访问数据的桥梁
触发器体内访问“正在被操作的那一行数据”,靠的是 NEW 和 OLD 两个关键字:
| 触发事件 | NEW 的含义 | OLD 的含义 |
|---|---|---|
| INSERT | 新插入的行 | 不存在,访问会报错 |
| UPDATE | 更新后的新行 | 更新前的旧行 |
| DELETE | 不存在,访问会报错 | 被删除的旧行 |
我用一个实例帮你建立直觉。假设 t_inventory 表有 product_id 和 stock 两个字段,现在要写一个触发器,禁止库存减到负数:
sql复制DELIMITER //
CREATE TRIGGER trg_inventory_before_update
BEFORE UPDATE ON t_inventory
FOR EACH ROW
BEGIN
IF NEW.stock < 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'stock cannot be negative';
END IF;
END //
DELIMITER ;
这里有个非常关键的技巧:在 BEFORE 触发器中,你可以修改 NEW 字段的值。MySQL 会用修改后的 NEW 值去完成实际的数据写入。这意味着你可以在数据落库前做“清洗”和“补全”。比如前面例子里,SET NEW.create_time = NOW() 就是典型的补全默认值。
但在 AFTER 触发器中,修改 NEW 是无效的,因为数据已经落库了。AFTER 触发器适合做日志、同步等“事后动作”。
2.4 触发器里的条件控制、变量和异常处理
触发器体内不仅仅可以写简单的赋值,还可以用 IF、CASE、变量、异常处理等。我做一个多条件判断的例子:
sql复制DELIMITER //
CREATE TRIGGER trg_orders_before_insert
BEFORE INSERT ON t_orders
FOR EACH ROW
BEGIN
DECLARE v_total DECIMAL(10,2);
-- 根据订单金额打标签
IF NEW.amount > 1000 THEN
SET NEW.order_level = 'HIGH';
ELSEIF NEW.amount > 100 THEN
SET NEW.order_level = 'MEDIUM';
ELSE
SET NEW.order_level = 'LOW';
END IF;
-- 查询其他表信息,做交叉校验
SELECT COALESCE(SUM(amount), 0) INTO v_total
FROM t_order_items
WHERE order_id = NEW.order_id;
IF v_total != NEW.amount THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'order amount mismatch';
END IF;
END //
DELIMITER ;
这里要注意变量的 DECLARE 必须放在 BEGIN 块的开头。另外,SELECT ... INTO 从其他表取数时,如果查不到记录,变量会被赋为 NULL,所以我在外面套了一层 COALESCE。
关于异常,MySQL 触发器里可以用 SIGNAL 主动抛错,也可以定义 DECLARE EXIT HANDLER 捕获异常做日志。一般业务场景用到 SIGNAL 就足够了。
3. 一个真实业务场景的完整设计:订单库存扣减实战
理论说再多,不如直接走一个完整需求。我拿电商里最常见的“下单扣库存”来演示触发器的完整设计思路。
3.1 场景描述与表结构设计
有两张表:t_order(订单表)和 t_inventory(库存表)。需求是:每新增一条订单,自动扣减对应商品的库存,同时记录一条库存变动日志到 t_stock_log。
sql复制CREATE TABLE t_order (
order_id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT NOT NULL,
quantity INT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE t_inventory (
product_id INT PRIMARY KEY,
stock INT NOT NULL,
version INT NOT NULL DEFAULT 0
) ENGINE=InnoDB;
CREATE TABLE t_stock_log (
log_id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT NOT NULL,
change_qty INT NOT NULL,
before_stock INT NOT NULL,
after_stock INT NOT NULL,
operate_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
3.2 触发器的设计思路
这里有个值得思考的细节:扣减库存应该用 BEFORE 还是 AFTER?
我推荐用 AFTER INSERT。原因很简单:AFTER 表示订单已经成功插入,此时再操作其他表,如果失败,整个事务会回滚,订单也会消失,不会出现“订单在但库存没扣”的状态。
我再结合库存表加一个乐观锁设计,避免并发下单时超卖:
sql复制DELIMITER //
CREATE TRIGGER trg_order_after_insert
AFTER INSERT ON t_order
FOR EACH ROW
BEGIN
DECLARE v_before_stock INT DEFAULT 0;
DECLARE v_after_stock INT DEFAULT 0;
-- 锁定库存行并读取当前库存
SELECT stock INTO v_before_stock
FROM t_inventory
WHERE product_id = NEW.product_id
FOR UPDATE;
IF v_before_stock < NEW.quantity THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'insufficient stock';
END IF;
SET v_after_stock = v_before_stock - NEW.quantity;
-- 更新库存
UPDATE t_inventory
SET stock = v_after_stock,
version = version + 1
WHERE product_id = NEW.product_id;
-- 写入库存变动日志
INSERT INTO t_stock_log (
product_id, change_qty, before_stock, after_stock
) VALUES (
NEW.product_id, NEW.quantity, v_before_stock, v_after_stock
);
END //
DELIMITER ;
3.3 为什么这样设计:几个关键决策点
第一个决策点是为什么读取库存时要 FOR UPDATE。不加锁的话,两个并发事务同时读到库存为 1,各自扣减 1,最终库存变成 -1,这就是超卖。加了 FOR UPDATE 后,第二个事务会阻塞等待第一个事务提交,读完库存已经是 0,随即触发 SIGNAL 报错。这里的锁粒度是行级,对于单商品下单的场景,性能损耗可以接受。
第二个决策点是为什么用 SIGNAL 报错而不是直接 UPDATE 负数。库存不足应该明确抛错,让业务层感知并给用户提示。直接改成 0 或负值虽然不报错,但会造成数据错误,后续对账困难。
第三个决策点是日志表和业务表在同一事务里写入。因为触发器内的操作和 INSERT 语句处于同一个隐式事务,日志表写入失败会导致整个订单回滚,数据一致性有保障。这也是触发器方案相比应用层方案更简单直接的地方。
3.4 并发下的极限性能与锁等待
这个设计在高并发下面临一个绕不开的问题:SELECT ... FOR UPDATE 会让同一商品的所有下单操作串行化。如果某一款爆款商品每秒有几百个订单,库存行的锁等待会非常严重,触发器的执行时间会被拉长,进而拖慢整个订单表的插入性能。
我在实际项目里遇到过这种情况,压测到每秒 500 个下单请求时,数据库出现明显的锁等待,平均响应时间从 20ms 飙升到 800ms。
解决方案有两种思路:
第一种是库存扣减从强一致降级为最终一致。不在下单事务里扣库存,而是把下单事件写入消息队列,由独立的库存服务异步扣减。流量高峰时可以先把订单存下来,库存服务慢慢消化。缺点是无法做到“下单即锁定库存”,可能会超卖。
第二种是热商品拆分库存行。比如某商品库存 1000,拆成 10 行,每行 100,下单时随机路由到某一行扣减。并发度提升了 10 倍,但订单表需要增加一个 inventory_slot 字段,复杂度上来了。
我的建议是:如果系统规模不大,并发下单量在每秒几十笔以内,触发器方案完全够用,逻辑清晰、一致性最强。如果已经需要应对大促流量,尽早切换到应用层 + 异步队列的方案,别让触发器成为瓶颈。
4. 触发器的调试与排查:没有调试器,怎么定位问题
触发器最让人头疼的是很难调试。它不像应用代码可以打日志、断点,MySQL 也没有专门为触发器提供调试器。我在实战里摸索出一套可行的排查方法。
4.1 建立触发器日志表:给触发器装一个“黑匣子”
最简单的调试手段,是在触发器里写日志表。我习惯在每一个关键分支都写一条日志,记录触发时间、表名、操作类型、NEW 和 OLD 的关键字段值。
sql复制CREATE TABLE trigger_debug_log (
id INT AUTO_INCREMENT PRIMARY KEY,
trigger_name VARCHAR(64),
table_name VARCHAR(64),
action_type VARCHAR(10),
affected_row_id INT,
extra_info VARCHAR(500),
log_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
然后在触发器里,临时加上日志写入:
sql复制DELIMITER //
CREATE TRIGGER trg_order_after_insert
AFTER INSERT ON t_order
FOR EACH ROW
BEGIN
DECLARE v_before_stock INT DEFAULT 0;
INSERT INTO trigger_debug_log (
trigger_name, table_name, action_type, affected_row_id, extra_info
) VALUES (
'trg_order_after_insert', 't_order', 'INSERT',
NEW.order_id,
CONCAT('product_id=', NEW.product_id, ', quantity=', NEW.quantity)
);
SELECT stock INTO v_before_stock
FROM t_inventory
WHERE product_id = NEW.product_id;
UPDATE t_inventory
SET stock = stock - NEW.quantity
WHERE product_id = NEW.product_id;
END //
DELIMITER ;
调试完记得把日志写入的语句删掉或注释掉,否则生产环境每笔订单都会多写一条日志,白白增加开销。
4.2 常见报错和根因分析
我在大量触发器使用过程中,遇到过三个高频报错,这里直接给你根因和解决方法。
报错 1:ERROR 1442 - Can't update table 'xx' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
这个错误的意思是:你正在触发器里更新触发器自己所属的那张表。比如在 AFTER INSERT ON t_order 的触发器里,又执行了一条 UPDATE t_order。MySQL 不允许这种“递归”操作,会直接报错。
根因通常是业务逻辑设计有问题。比如你想在插入订单后更新订单的某个字段,这在 AFTER 触发器里做不到,改用 BEFORE 触发器直接修改 NEW 字段即可。
报错 2:ERROR 1362 - Updating of NEW row is not allowed in after trigger.
这个错误的意思是:你在 AFTER 触发器里试图修改 NEW 字段的值。AFTER 阶段数据已经落库,修改无效。解决方法是把逻辑挪到 BEFORE 触发器里执行。
报错 3:ERROR 1054 - Unknown column 'OLD.xxx' in 'NEW'.
这个错误通常是在 INSERT 触发器里访问了 OLD 字段,或者在 DELETE 触发器里访问了 NEW 字段。INSERT 没有旧行,DELETE 没有新行,逻辑上就不存在这些值。
4.3 查看触发器的执行状态
MySQL 提供了几个系统表来查看触发器的元信息:
sql复制-- 查看某个库下所有触发器
SHOW TRIGGERS FROM your_database;
-- 查看触发器的定义语句
SHOW CREATE TRIGGER your_database.trg_order_after_insert;
-- 通过 information_schema 查询
SELECT * FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'your_database' \G
我个人的习惯是直接用 information_schema 查询,因为它可以看到触发器定义时的 SQL 语句、创建时间、字符集等信息,排查问题更快。
5. 触发器的管理与迁移:从开发库到生产环境的完整路径
触发器写好后,怎么从开发环境平滑地迁移到生产环境,这是一个容易被忽略、但坑特别多的环节。我结合 DBeaver、Navicat、HeidiSQL 等常用工具的使用经验来聊。
5.1 使用 GUI 工具查看和导出触发器
团队里开发人员水平参差不齐,不是每个人都会用命令行查看触发器,所以 GUI 工具是主流。
DBeaver:连接数据库后,展开数据库节点,找到“触发器”子节点,右键就能看到所有触发器列表。导出时,右键对应触发器,选择“生成 SQL”或“导出 DDL”,DBeaver 会自动生成完整的 CREATE TRIGGER 语句。我特别喜欢 DBeaver 的一点是,它可以一次性导出整个库的存储过程、函数和触发器,右键数据库节点 → 生成 SQL → 选择对象类型即可。这个功能在从测试库导出到生产库时特别好用,配合团队里的 CI 流程,可以快速完成结构迁移。
Navicat:在上方菜单栏找到“工具” → “数据传输”,或者在对象列表里右键选择“导出 SQL 文件”,勾选“触发器”选项。Navicat 导出的文件里会自动包含 DROP TRIGGER IF EXISTS 语句,这是一个很好的习惯,导入的时候不会因为触发器已存在而报错。
HeidiSQL:连接数据库后,右侧会发现一个“触发器和事件”的标签页。HeidiSQL 的导出功能是集成在会话级导出里的:右键数据库名 → “导出 SQL 转储”,勾选“触发器”。另外,HeidiSQL 还支持批量选择多个数据库对象,生成一个包含全部对象的 SQL 文件,方便做整库迁移。
无论用哪个工具,导出后一定要人工检查一遍。工具生成的语句可能在
DELIMITER处理上和在数据库里执行时不同,尤其是一些嵌套用了触发器体内分号的场景,导入时很容易因为分号被截断而报错。
5.2 手动编写版本化迁移脚本:比 GUI 更可靠的方式
团队协作时,我强烈建议把触发器脚本纳入版本管理。一般项目里会有 migration 目录,专门存放数据库结构变更脚本。一个触发器的变更版本文件长这样:
bash复制migrations/
20240515100000_create_trg_order_after_insert.sql
脚本内容:
sql复制-- 20240515 创建订单后扣减库存触发器
-- 变更原因:修复并发下单导致超卖问题
-- 负责人:某某
DROP TRIGGER IF EXISTS trg_order_after_insert;
DELIMITER //
CREATE TRIGGER trg_order_after_insert
AFTER INSERT ON t_order
FOR EACH ROW
BEGIN
DECLARE v_before_stock INT DEFAULT 0;
SELECT stock INTO v_before_stock
FROM t_inventory
WHERE product_id = NEW.product_id
FOR UPDATE;
IF v_before_stock < NEW.quantity THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'insufficient stock';
END IF;
UPDATE t_inventory
SET stock = v_before_stock - NEW.quantity
WHERE product_id = NEW.product_id;
END //
DELIMITER ;
这段脚本里特意包含了 DROP TRIGGER IF EXISTS,这样脚本可以重复执行,方便做回滚和重放。
5.3 生产环境更新触发器的注意事项
生产环境更新触发器,以下几条是我用真金白银换来的教训。
第一,评估锁表风险。 在 MySQL 8.0 之前,创建或删除触发器时会对表加元数据锁(MDL),如果表正在被大量写入,操作可能会阻塞。如果业务表很大且持续有写入,建议在业务低峰期操作。
第二,先备份现有触发器定义。 操作前先执行 SHOW CREATE TRIGGER,把输出保存下来,方便出问题时立即回滚。
第三,观察慢查询和错误日志。 触发器上线后,立即监控一段时间,看有没有产生新的慢查询或者 SQL 错误。我遇到过触发器上线后,因为 SELECT 语句没走索引,导致每次插入订单都触发全表扫描的情况,业务直接卡死。
第四,注意字符集和排序规则。 如果触发器里有字符串比较,确保表的字符集和生产环境一致,否则可能因为 collation 不兼容导致执行错误。
5.4 从生产导出触发器到测试环境的反方向迁移
最后说一个反向场景:从生产环境把触发器导出到测试环境。这时候最重要的是数据脱敏和生产对象梳理。生产库里的触发器数量可能很多,但测试环境不一定每张表都存在,导出后直接导很可能报错“Table doesn't exist”。
正确的做法是:
- 用查询语句先梳理生产库里所有触发器和对应表的清单:
sql复制SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'your_production_db'
ORDER BY EVENT_OBJECT_TABLE;
- 对比测试环境已有的表,只导出对应的触发器,避免缺表报错。
- 导出后用文本编辑器全局检查一遍,确认没有生产环境特有的变量或链接信息。
6. 性能陷阱与替代方案:什么时候该放弃触发器
前面提到过,触发器的性能瓶颈主要来自行级执行。我在生产环境压测过一组数据,可以直观地看到差别。
6.1 批量更新场景的性能对比
假设 t_order 表有 10 万条数据,t_inventory 表是关联的库存表。两条方案的目的是:把所有订单的 status 改成 “已完成” 的同时,把对应商品库存加上订单数量。
方案 A:在 t_order 表上加一个 AFTER UPDATE 触发器,每次更新都去改库存。
方案 B:不用触发器,直接写两条 SQL 在应用层事务里完成。
实测结果(以本机 MySQL 8.0,5000 行批量 UPDATE 为例):
| 方案 | 平均耗时 | 数据库锁等待 | 代码复杂度 |
|---|---|---|---|
| 触发器方案 | 2.3 秒 | 明显,库存表长时间被锁 | 低,写入逻辑封装在库里 |
| 应用层方案 | 0.4 秒 | 相对较小 | 中,需要维护两表一致性 |
差别非常明显。批量 UPDATE 时,触发器逐行执行、逐行加锁、逐行访问其他表,性能被放大了 N 倍。
6.2 触发器内部 SQL 的性能优化建议
如果确实要用触发器,以下几点可以显著降低性能损耗:
- 触发器里的 SQL 必须走索引。 比如
WHERE product_id = NEW.product_id,product_id必须有索引,否则每行触发都做全表扫描。 - 避免在触发器内做聚合查询。
SELECT SUM(...)这类操作在触发器里执行,数据量大时极其耗时。如果一定要做,考虑改成维护一张实时汇总表,在事务提交时只做增量更新。 - 触发器内不要调用存储过程或函数。 存储过程或函数内部的 SQL 可能带来额外开销,而且调用链一长,排查问题就变得困难。
- 一张表上的触发器数量控制在 2 个以内。 每个触发器本身有执行成本,触发器多了,写操作的开销线性叠加。
6.3 什么时候用应用层替代触发器
我总结了几条“触发器慎用”的判断标准,如果命中 2 条以上,建议考虑应用层方案:
- 业务逻辑复杂,需要根据多种条件进行不同的后续操作;
- 涉及网络调用,比如扣库存后需要调用第三方接口发货;
- 对性能要求极高,单表写入量达到每秒几百条以上;
- 团队流动性高,需要让每个开发者在代码里都能看到完整的链路;
- 需要在触发器内访问远程数据库或外部系统(触发器本身不支持,只能通过 UDF 绕过,但那是一个更大的坑)。
以扣库存为例,更现代且容易维护的替代方案是:应用层在同一个本地事务里先插入订单,再更新库存,最后判断更新影响行数决定是否回滚。代码可读性和可测试性都更强。
6.4 如果一定要用触发器,MySQL 8.0 的改进点
MySQL 8.0 相比 5.7 在触发器方面没有革命性变化,但有一个细节值得注意:8.0 支持在触发器中使用窗口函数和公共表表达式(CTE),让复杂计算写起来更清晰。另外,8.0 增强了原子性 DDL,创建或删除触发器本身不会因为中间报错而留下半成品状态,操作更安全。
如果你还在 MySQL 5.7 上,升级到 8.0 是值得考虑的方向,不仅是触发器,整体的 SQL 执行性能都有明显提升。
7. 关于触发器的命名规范与团队协作
这是个偏“软技能”的话题,但实际项目里坑非常多。触发器命名如果没有统一规范,半年后没人知道每个触发器是干嘛的。
我建议的命名格式是:
code复制trg_{表名}_{触发时机}_{触发事件}_{用途标识}
比如:
trg_order_after_insert_decrement_stock:订单表插入后扣减库存trg_user_before_update_refresh_time:用户表更新前刷新时间戳trg_payment_after_insert_write_audit:支付表插入后写审计日志
命名规范在团队协作里的价值,主要体现在排查问题和做代码审查的时候。看到名字就能大概猜出触发器的功能,不用一个个打开看定义。
另外,触发器是数据库对象,代码评审时一定要纳入审查范围。我见过很多团队只评审应用代码,数据库变更 DBA 收到就执行,执行完就完事。等到问题发生,才发现当时审查时根本没注意到触发器里隐藏的SELECT ... FOR UPDATE这种重量级操作。
如果你也在用 CI/CD 流程管理数据库变更,建议把触发器脚本纳入流水线,跟应用代码一起走测试、评审、发布的完整流程。这样每个触发器的变更都有迹可循,出问题也能快速回滚。
最后再分享一个小技巧:写触发器时,尽量用 DEFINER 指定一个专用的数据库账号,而不是用 root。这样即使团队里有人使用普通开发账号执行触发语句,触发器内部也能正常访问其他表。同时,专用账号的权限隔离,也让整个数据库更加安全可控。
