📖 本篇简介:存储过程要手动 CALL 才执行,本篇讲两种"自动触发"的数据库对象——触发器(Trigger)在表发生增删改时自动运行,事件调度(Event)按时间计划自动运行。学完后你能实现"下单自动写日志""每天凌晨清理过期数据"这类自动化逻辑。本示例基于 MySQL 8.0 语法。目录
- 1 触发器是什么
- 2 BEFORE与AFTER触发器
- 3 触发器的创建查看与删除
- 4 事件调度器EVENT
- 5 使用场景与典型案例
- 6 触发器与事件速查表
- 7 常见坑与注意事项
- 8 练习题
1 触发器是什么
触发器(Trigger)是与表绑定的一段程序,当这张表发生 INSERT、UPDATE、DELETE 操作时,MySQL 自动执行它。它不需要手动调用,由数据库"事件驱动"。
和"应用层写日志"相比,触发器的好处是无论哪个入口(后台脚本、人工改库、批量导入)动了表,日志都会被记录,不会漏。代价是它对开发者不可见、有性能成本,所以要克制使用。
典型用途:
- 订单插入后,自动往日志表写一条记录;
- 用户更新信息时,自动记录修改时间;
- 删除订单时,自动做关联清理或归档;
- 每天凌晨自动清理过期的临时数据。
| 对比 | 存储过程 | 触发器 |
|---|---|---|
| 执行方式 | 手动 CALL | 表增删改时自动执行 |
| 绑定 | 独立对象 | 绑定到某张表 |
| 传参 | 支持 IN/OUT | 无参数,靠 NEW/OLD 行引用 |
触发器里最关键的两个引用是 NEW 和 OLD:
NEW:代表刚插入(INSERT)或更新后(UPDATE)的新行;OLD:代表更新前(UPDATE)或被删除(DELETE)的旧行。
不同事件类型下,NEW 和 OLD 能不能用是固定的:
| 事件类型 | NEW 可用 | OLD 可用 |
|---|---|---|
| INSERT | 是(新插入的行) | 否 |
| UPDATE | 是(更新后的行) | 是(更新前的行) |
| DELETE | 否 | 是(被删除的行) |
记住这张表,写触发器时就不会乱用引用了。
2 BEFORE与AFTER触发器
触发器分两个时机:
| 时机 | 含义 | 典型用途 |
|---|---|---|
| BEFORE | 在数据写入前触发 | 校验、改写新值(如自动填默认值) |
| AFTER | 在数据写入后触发 | 写日志、级联操作 |
BEFORE 和 AFTER 选哪个,就看你想"拦下来改"还是"写完了补动作"。要改数据值用 BEFORE,要在数据落库后做附加动作就用 AFTER。
事件类型又分 INSERT、UPDATE、DELETE,组合起来就是 BEFORE INSERT、AFTER UPDATE 等。
2.1 AFTER INSERT:下单写日志
-- 插入订单后自动写日志(MySQL 8.0)
DELIMITER $$
CREATE TRIGGER trg_order_log
AFTER INSERT ON orders
FOR EACH ROW -- 每受影响一行触发一次
BEGIN
INSERT INTO order_log(order_id, action, create_time)
VALUES(NEW.order_id, '下单', NOW()); -- NEW 代表刚插入的新行
END$$
DELIMITER ;
以后任何 INSERT INTO orders 成功,order_log 里就多一条记录,应用代码完全无感。
2.2 BEFORE INSERT:自动补默认值
-- 插入前自动填创建时间(MySQL 8.0)
DELIMITER $$
CREATE TRIGGER trg_set_time
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
IF NEW.create_time IS NULL THEN
SET NEW.create_time = NOW(); -- 修改 NEW 的字段值
END IF;
END$$
DELIMITER ;
BEFORE 触发器里可以 SET NEW.字段 = 值,把要写入的数据"拦下来改一改"再放行。
2.3 AFTER DELETE:归档旧数据
-- 删除订单前把旧行归档(MySQL 8.0)
DELIMITER $$
CREATE TRIGGER trg_order_archive
AFTER DELETE ON orders
FOR EACH ROW
BEGIN
INSERT INTO orders_archive
VALUES(OLD.order_id, OLD.amount, NOW()); -- OLD 是被删掉的旧行
END$$
DELIMITER ;
2.4 FOR EACH ROW:行级触发
MySQL 触发器是行级的:FOR EACH ROW 表示每条受影响的行都触发一次。如果一条 INSERT ... SELECT 插入了 1000 行,触发器就会执行 1000 次,每次的 NEW 对应当前那一行。
-- 批量插入也逐行触发(MySQL 8.0)
INSERT INTO orders(amount) VALUES(10),(20),(30); -- 触发器执行 3 次
这也是为什么批量大事务要谨慎——触发器会把单行成本放大 N 倍。
2.5 用 SIGNAL 在触发器里拦截非法数据
BEFORE 触发器可以做校验,不满足条件就主动报错、拒绝写入:
-- 金额不能为负,否则拦截(MySQL 8.0)
DELIMITER $$
CREATE TRIGGER trg_check_amount
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
IF NEW.amount < 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '订单金额不能为负数';
END IF;
END$$
DELIMITER ;
SIGNAL SQLSTATE '45000' 是自定义错误,后面的 INSERT 会被中止并提示这条消息。这是用触发器做"数据库层校验"的典型用法。
3 触发器的创建查看与删除
-- 查看库里有哪些触发器(MySQL 8.0)
SHOW TRIGGERS;
-- 查 information_schema
SELECT trigger_name, event_manipulation, event_object_table
FROM information_schema.TRIGGERS
WHERE trigger_schema = '你的库名';
-- 删除触发器(MySQL 8.0)
DROP TRIGGER IF EXISTS trg_order_log;
注意:一张表上,同一种时机+事件的触发器在 MySQL 8.0 里可以有多个,但要小心它们的执行顺序。一般一张表同类型触发器一个就够,多了维护困难。
3.1 触发器与表一起导出
导出表结构时,触发器不会自动跟 mysqldump 表名 走,要用 --triggers 选项(默认开启,但显式加上更安心)。迁移到新库后,先 SHOW TRIGGERS 确认触发器都在,别等线上"插单不写日志"才发现漏了。
4 事件调度器EVENT
事件调度器(Event Scheduler)是 MySQL 自带的"定时任务",像 Linux 的 crontab,按时间计划自动执行 SQL。它适合"每天凌晨跑一次""每小时汇总一次"这类周期性任务。
4.1 先确认事件调度器开着
-- 查看事件调度器状态(MySQL 8.0)
SHOW VARIABLES LIKE 'event_scheduler';
-- 如果是 OFF,打开它(需要 SUPER 权限)
SET GLOBAL event_scheduler = ON;
event_scheduler=ON 时,MySQL 后台才会按计划触发事件。这个参数是全局的,不开的话所有事件都不会执行,排查"事件为什么不跑"先看它。
4.2 创建一个定时事件
-- 每天凌晨 1 点清理 90 天前的日志(MySQL 8.0)
DELIMITER $$
CREATE EVENT evt_clean_log
ON SCHEDULE EVERY 1 DAY -- 每 1 天执行一次
STARTS '2026-10-08 01:00:00' -- 从这个时间开始
DO
BEGIN
DELETE FROM order_log
WHERE create_time < DATE_SUB(NOW(), INTERVAL 90 DAY);
END$$
DELIMITER ;
4.3 只执行一次的事件
有些任务只要跑一次(比如初始化某张表),用 AT:
-- 在指定时间执行一次后自动结束(MySQL 8.0)
CREATE EVENT evt_once
ON SCHEDULE AT '2026-10-10 12:00:00'
DO
INSERT INTO backup_log(msg) VALUES('一次性任务完成');
补充一点:一次性事件执行完后,默认会被 MySQL 自动删除;如果想让它执行后仍保留在事件列表里(便于审计),可以显式加 ON COMPLETION PRESERVE。周期事件则会一直按计划跑,直到你 DROP 或 DISABLE 它。
4.4 查看与删除事件
-- 查看事件(MySQL 8.0)
SHOW EVENTS;
-- 临时停用 / 重新启用事件
ALTER EVENT evt_clean_log DISABLE;
ALTER EVENT evt_clean_log ENABLE;
-- 删除事件
DROP EVENT IF EXISTS evt_clean_log;
4.5 事件的时间单位
EVERY 后面支持的间隔单位很灵活:
| 写法 | 含义 |
|---|---|
| EVERY 1 HOUR | 每小时 |
| EVERY 1 DAY | 每天 |
| EVERY 1 WEEK | 每周 |
| EVERY 30 MINUTE | 每 30 分钟 |
| EVERY 1 DAY STARTS '2026-10-08 01:00:00' | 每天 1 点(配合 STARTS 指定首次时间) |
还可以用 ENDS '时间' 指定事件到期后自动停止,避免一个临时任务无限跑下去。
4.6 事件 vs 操作系统 crontab
既然 Linux 有 crontab,为什么还要用数据库自带的事件调度?
| 对比 | 数据库 Event | 系统 crontab |
|---|---|---|
| 部署依赖 | 跟着数据库走 | 依赖某台机器 |
| 与数据同生命周期 | 库在任务在 | 机器挂了任务就漏 |
| 适合 | 轻量、纯 SQL 的定时任务 | 需要脚本、复杂逻辑的任务 |
经验:纯 SQL 的简单定时任务用 Event 足够;需要调脚本、做复杂逻辑、发通知的任务,用系统 crontab 更合适,别硬塞进数据库。
另外,事件在执行时运行的用户权限取决于事件的 DEFINER,跨库迁移时若定义者账号不存在,事件会直接报错,迁移脚本里要一并处理账号。
5 使用场景与典型案例
| 需求 | 用触发器还是事件 |
|---|---|
| 下单后写操作日志 | AFTER INSERT 触发器 |
| 更新时自动改 update_time | BEFORE/AFTER UPDATE 触发器 |
| 每天凌晨清理过期数据 | 每日事件 |
| 每小时汇总报表入中间表 | 每小时事件 |
| 删除订单时归档到历史表 | AFTER DELETE 触发器 |
实战中要分清:"由数据变更驱动"用触发器,"由时间驱动"用事件。别用触发器去做周期性统计——那是事件的活;也别用事件去保证"每次下单都有日志"——那得靠触发器(或应用层事务)。
5.1 完整案例:下单后自动记日志
把前面的片段串成一个可落地的小场景。假设表结构如下:
-- 配套表结构(MySQL 8.0)
CREATE TABLE orders(
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
amount DECIMAL(10,2),
create_time DATETIME
);
CREATE TABLE order_log(
log_id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT,
action VARCHAR(20),
create_time DATETIME
);
建好 AFTER INSERT 触发器后,插入订单:
-- 插入订单(MySQL 8.0)
INSERT INTO orders(amount, create_time) VALUES(99.5, NOW());
orders 多一行的同时,order_log 也自动多一条 order_id=1, action='下单' 的记录。这就是触发器"无感自动化"的价值——业务代码只写了一句 INSERT,审计日志却自动齐全。
5.2 完整案例:定时归档
事件这边也串一个场景。每月 1 号凌晨 2 点,把上一个月已完成的订单归档到历史表:
-- 每月归档(MySQL 8.0)
DELIMITER $$
CREATE EVENT evt_monthly_archive
ON SCHEDULE EVERY 1 MONTH
STARTS '2026-11-01 02:00:00'
DO
BEGIN
INSERT INTO orders_history(order_id, amount, finish_time)
SELECT order_id, amount, finish_time
FROM orders
WHERE status = 3
AND finish_time >= DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 MONTH), '%Y-%m-01')
AND finish_time < DATE_FORMAT(NOW(), '%Y-%m-01');
END$$
DELIMITER ;
配合 event_scheduler=ON,这个归档动作每月自动跑,不用人盯。
6 触发器与事件速查表
| 操作 | 语句 |
|---|---|
| 建 AFTER INSERT 触发器 | CREATE TRIGGER trg AFTER INSERT ON t FOR EACH ROW BEGIN ... END; |
| 引用新行 / 旧行 | NEW.列 / OLD.列 |
| 改写入值(BEFORE) | SET NEW.列 = 值; |
| 查看触发器 | SHOW TRIGGERS; |
| 删除触发器 | DROP TRIGGER IF EXISTS trg; |
| 开事件调度器 | SET GLOBAL event_scheduler = ON; |
| 周期事件 | ON SCHEDULE EVERY 1 DAY STARTS ... |
| 一次性事件 | ON SCHEDULE AT '时间' |
| 停用/启用事件 | ALTER EVENT evt DISABLE / ENABLE; |
| 查看事件 | SHOW EVENTS; |
7 常见坑与注意事项
1. 触发器是"隐式"的:业务代码里看不到触发器,新人接手时经常疑惑"为什么插一行表,日志表也多了一行"。新环境部署要把触发器、事件随表结构一起导出来。
2. 触发器里别写重逻辑:触发器会拖慢触发它的那条 SQL。一条 INSERT 后面挂三个触发器,写入性能肉眼可见地掉。
3. BEFORE 里改 NEW 才生效:AFTER 触发器里 SET NEW.列 是没意义的——数据已经写进去了。要改写值必须在 BEFORE。
4. 事件调度器没开:建了事件却不跑,先查 event_scheduler 是不是 OFF;重启后配置可能被重置,要在配置文件里固化。
5. 跨主从复制的事件:主库上的事件会复制到从库,要防止从库也跑一遍,通常在从库禁用事件。
6. 触发器递归/连锁:触发器里又操作了别的表(甚至同一张表),可能引发连锁触发,排查问题时容易绕晕。设计时保持触发器"小而专"。
7. 别用触发器代替事务:跨表一致性该用事务(下一篇讲),触发器只是自动动作,出错了照样要考虑回滚。
8. 触发器里不能操作触发它的同一张表:在 orders 的触发器里再 UPDATE orders,MySQL 会报错或行为异常,绕一圈回到自己容易死锁。
9. 事件执行失败没告警:事件悄悄失败不会发通知,重要定时任务要配合日志表和监控,别以为"建了事件就万事大吉"。
10. 触发器里做重 IO:触发器在业务请求的事务里同步执行,里面若再发网络请求、写大日志,会直接拖慢线上接口。触发器只做轻量、确定性的数据库操作。
8 练习题
练习 1:写一个 AFTER UPDATE 触发器
当 goods 表的价格被更新时,往 price_log 表记录 商品ID、旧价、新价、修改时间。
<details>
<summary>参考答案</summary>
-- 价格变更日志(MySQL 8.0)
DELIMITER $$
CREATE TRIGGER trg_price_log
AFTER UPDATE ON goods
FOR EACH ROW
BEGIN
IF OLD.price <> NEW.price THEN
INSERT INTO price_log(goods_id, old_price, new_price, change_time)
VALUES(NEW.goods_id, OLD.price, NEW.price, NOW());
END IF;
END$$
DELIMITER ;
</details>
练习 2:自动填充更新时间
写一个触发器,每次 user_info 被 UPDATE 时,自动把 update_time 设为当前时间。
<details>
<summary>参考答案</summary>
-- 自动更新时间(MySQL 8.0)
DELIMITER $$
CREATE TRIGGER trg_update_time
BEFORE UPDATE ON user_info
FOR EACH ROW
BEGIN
SET NEW.update_time = NOW();
END$$
DELIMITER ;
</details>
练习 3:建一个每小时事件
写一个事件,每小时把 orders 的当小时汇总写入 hourly_stat。
<details>
<summary>参考答案</summary>
-- 每小时汇总(MySQL 8.0)
DELIMITER $$
CREATE EVENT evt_hourly
ON SCHEDULE EVERY 1 HOUR
DO
BEGIN
INSERT INTO hourly_stat(hour_start, total_amt, order_cnt)
SELECT DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 HOUR), '%Y-%m-%d %H:00:00'),
SUM(amount), COUNT(*)
FROM orders
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR);
END$$
DELIMITER ;
</details>
练习 4:删除与查看
写出"查看所有触发器"和"删除触发器 trg_order_log"的语句。
<details>
<summary>参考答案</summary>
-- 查看与删除(MySQL 8.0)
SHOW TRIGGERS;
DROP TRIGGER IF EXISTS trg_order_log;
</details>
练习 5:一次性初始化事件
写一个事件,在 2026-10-15 03:00:00 这个时间点执行一次:往 init_log 插入一条"初始化完成"记录,之后自动结束。
<details>
<summary>参考答案</summary>
-- 一次性事件(MySQL 8.0)
CREATE EVENT evt_init
ON SCHEDULE AT '2026-10-15 03:00:00'
DO
INSERT INTO init_log(msg) VALUES('初始化完成');
</details>
本章小结
触发器和事件是数据库的"自动化引擎":触发器跟着数据走、事件跟着时间走。它们能让很多运维和审计逻辑免代码自动完成,但也容易变成"看不见的复杂度"——用的时候保持轻量,并确保新环境能完整迁移这些对象。判断用哪一个的口诀很简单:数据一变就触发的用触发器,时间一到就执行的用事件。至此,SQL 的基本武器就齐了,下一篇进入事务与隔离级别,解决"多个操作要么全成功、要么全失败"的问题。
下一篇预告:第 14 章 事务与隔离级别
文章回复
0 条公开回复