RSS
菜单
全部文章快讯开发科技深度热点

第 13 章 触发器与事件调度

内容摘要

存储过程要手动 `CALL` 才执行,本篇讲两种"自动触发"的数据库对象——触发器(Trigger)在表发生增删改时自动运行,事件调度(Event)按时间计划自动运行。学完后你能实现"下单自动写日志""每天凌晨清理过期数据"这类自动化逻辑。本示例基于 MySQL 8.0 语法。

📖 本篇简介:存储过程要手动 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 章 事务与隔离级别

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 13 章 触发器与事件调度》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。