📝 触发器和事件 · 习题课

📚 第16章配套练习 💻 含代码实操 🎯 期末重点
🎯 本节练习目标
  • 掌握 BEFORE/AFTER、NEW/OLD、AT/EVERY 等核心概念
  • 读懂 一段触发器或事件代码,预测执行结果
  • 能根据需求 写出 触发器和事件
  • 会综合应用:从业务场景设计触发器

📌 答题前先看:触发器写作模板

🎯 标准模板

DELIMITER $$
CREATE TRIGGER 触发器名
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON 表名
FOR EACH ROW
BEGIN
    -- 触发时执行的 SQL
END $$
DELIMITER ;

⚠️ 三个最容易踩的坑

NEW/OLD 用错 —— 插入用 NEW、删除用 OLD、修改两个都能用

BEFORE/AFTER 选错 —— 想改新值用 BEFORE、想记日志用 AFTER

事件忘开调度器 —— SET GLOBAL event_scheduler = ON

📝 第一部分 · 概念巩固

先用几道选择题把核心概念过一遍。

练习 1.1 ⭐⭐ 必考 NEW / OLD

下列代码中,哪个会引起报错

📖 查看答案

✅ 错误的是:B

INSERT 触发器中没有 OLD(因为是新插入的记录,没有"旧值"可言)。

对应关系表:

INSERT:只有 NEW(新插入的)

UPDATE:NEW(新值)+ OLD(旧值)都有

DELETE:只有 OLD(被删除的)

口诀:"插入用 NEW,删除用 OLD,修改两个都能用"

练习 1.2 ⭐⭐ 必考 BEFORE vs AFTER

需求:"插入员工记录时,如果工资 (salary) 小于 0,自动改为 0 再保存"。下列触发器类型 正确 的是?

📖 查看答案

✅ 答案:A · BEFORE INSERT

分析:

① 要 修改新值(小于 0 改成 0)→ 必须用 BEFORE(AFTER 时数据已经写入,再改 NEW 也来不及)

插入事件 → INSERT

所以是 BEFORE INSERT

口诀:"先校验用 BEFORE,后记录用 AFTER"

练习 1.3 ⭐⭐ 必考 触发器 vs 事件

下列需求中,必须使用事件(不能用触发器) 实现的是?

📖 查看答案

✅ 答案:B

判断标准:

"由数据操作触发"(INSERT/UPDATE/DELETE)→ 用 触发器

"由时间触发"(每天/每小时/到点)→ 用 事件

分析:

A:用户注册 = 表的 INSERT → 触发器

B:每天凌晨 3 点 = 时间到 → 事件

C:删除订单 = 表的 DELETE → 触发器

D:修改价格 = 表的 UPDATE → 触发器

口诀:"触发器响应数据变化,事件响应时间到来"

练习 1.4 ⭐⭐ 中等 AT vs EVERY

需求:"明天上午 10 点把临时表 temp 清空(只清一次)"。下列哪种 SCHEDULE 写法 正确

📖 查看答案

✅ 答案:B

判断要点:

① 题目说"只清一次" → 用 AT(一次性)

② AT 后面跟 具体的时间字符串,格式 'YYYY-MM-DD HH:MM:SS'

分析:

A 错:EVERY 是周期性,会"每天都清空"

C、D 错:MySQL 不识别 TOMORROW、NEXT MORNING 这种自然语言

口诀:"AT = 在某时(一次),EVERY = 每隔(周期)"

练习 1.5 ⭐⭐ 中等 综合辨析

关于触发器和事件,下列说法 错误 的是?

📖 查看答案

✅ 错的是:C

事件没有 NEW 和 OLD。NEW/OLD 是触发器特有的,因为触发器关联到具体的"行"操作。

事件是"到时间就跑",并不关联任何具体的行,所以没有 NEW/OLD 概念。

📖 第二部分 · 读代码(预测结果)必考

本部分给一段代码,不要急着运行,先动脑分析执行流程,再对照答案。

练习 2.1 📖 读代码 BEFORE INSERT

已经创建了下面的触发器,执行 INSERT 后,sc 表中插入的记录长什么样

SQL · 触发器定义
DELIMITER $$
CREATE TRIGGER check_score
BEFORE INSERT ON sc
FOR EACH ROW
BEGIN
    IF NEW.score IS NULL THEN
        SET NEW.score = 0;
    ELSEIF NEW.score > 100 THEN
        SET NEW.score = 100;
    END IF;
END $$
DELIMITER ;

-- 然后执行:
INSERT INTO sc VALUES ('s1', 'c1', NULL);
INSERT INTO sc VALUES ('s2', 'c2', 85);
INSERT INTO sc VALUES ('s3', 'c3', 120);
📖 查看答案

逐条分析:

第一条 INSERT (s1, c1, NULL)

NEW.score = NULL → 走 IF 分支 → NEW.score 改为 0

实际写入:('s1', 'c1', 0)

第二条 INSERT (s2, c2, 85)

NEW.score = 85 → IF 不满足(不是 NULL),ELSEIF 不满足(不大于 100)→ NEW.score 不改

实际写入:('s2', 'c2', 85)

第三条 INSERT (s3, c3, 120)

NEW.score = 120 → IF 不满足,ELSEIF 满足(120 > 100)→ NEW.score 改为 100

实际写入:('s3', 'c3', 100)

▶ 最终 sc 表中的数据 sno cno score s1 c1 0 s2 c2 85 s3 c3 100
💡 知识点

BEFORE 触发器中可以修改 NEW,会真的改变写入的数据。这就是为什么"数据校验和清洗"通常用 BEFORE 触发器。

练习 2.2 📖 读代码 AFTER UPDATE · NEW/OLD

已有触发器 + 学生表数据如下,执行 UPDATE 后,update_log 表中会插入什么记录

SQL · 现有数据和触发器
-- 学生表 s 中现有数据:
--   sno='s1'  sn='张三'  maj='信息'

DELIMITER $$
CREATE TRIGGER log_update
AFTER UPDATE ON s
FOR EACH ROW
BEGIN
    INSERT INTO update_log VALUES
    (OLD.sno, OLD.maj, NEW.maj, NOW());
END $$
DELIMITER ;

-- 然后执行(假设 NOW() 此时是 '2024-12-20 10:00:00'):
UPDATE s SET maj = '计算机' WHERE sno = 's1';
📖 查看答案

分析过程:

UPDATE 操作

把 s1 的 maj 从 '信息' 改成 '计算机'

OLD = (sno='s1', sn='张三', maj='信息') ← 旧值

NEW = (sno='s1', sn='张三', maj='计算机') ← 新值

触发器执行

插入 update_log,对应字段值:

① OLD.sno → 's1'

② OLD.maj → '信息'(旧专业)

③ NEW.maj → '计算机'(新专业)

④ NOW() → '2024-12-20 10:00:00'

▶ update_log 表新增记录 sno omaj nmaj udate s1 信息 计算机 2024-12-20 10:00:00
💡 知识点

UPDATE 触发器中,OLD 和 NEW 都可用 —— 一个是修改前的,一个是修改后的

② 这就是"记日志"的典型套路:用 OLD 拿旧值,用 NEW 拿新值,加上时间戳,全部塞到日志表

练习 2.3 📖 读代码 事件 · EVERY

阅读下面的事件代码,分析它的作用,并指出可能存在的问题。

SQL · 事件代码
CREATE EVENT clean_old_logs
ON SCHEDULE EVERY 1 DAY
DO
DELETE FROM login_log
WHERE login_time < NOW() - INTERVAL 30 DAY;
📖 查看答案

事件的作用:

解读 SCHEDULE

EVERY 1 DAY = 每天执行一次(周期性事件)

解读 DO 部分

DELETE FROM login_log WHERE login_time < NOW() - INTERVAL 30 DAY

= 删除 login_log 表中 30 天前的登录记录

总结

这个事件的作用:每天自动清理 30 天前的登录日志,避免日志表无限增长。

⚠️ 潜在问题

事件调度器没开就不会执行 —— 必须先 SET GLOBAL event_scheduler = ON

② 没有指定 STARTS,事件会从创建时立即开始计时,可能跟预期的执行时间不同

③ 实际生产中,应该指定凌晨低峰时段执行,避免影响业务

💻 第三部分 · 写代码必考

本部分要求 根据需求自己写触发器或事件。先在纸上画出框架,再对照答案。

练习 3.1 💻 写代码 BEFORE INSERT

需求:在员工表 emp 上创建一个触发器,插入员工时,如果工资 (salary) 小于 0,自动改为 0;如果大于 100000,自动改为 100000。

📖 查看答案

解题思路:

① 要"修改新值" → 必须用 BEFORE

② 是 插入 时校验 → INSERT

③ 用 NEW.salary 拿到要插入的工资值,用 SET NEW.salary = ... 修改

SQL · 标准答案
DELIMITER $$

CREATE TRIGGER check_salary
BEFORE INSERT ON emp
FOR EACH ROW
BEGIN
    IF NEW.salary < 0 THEN
        SET NEW.salary = 0;
    ELSEIF NEW.salary > 100000 THEN
        SET NEW.salary = 100000;
    END IF;
END $$

DELIMITER ;
💡 注意点

① 别忘了 DELIMITER —— 触发器内部有多条以 ; 结尾的语句

FOR EACH ROW 必写

SET NEW.salary = ... 真的会改写入的值,这就是 BEFORE 的妙用

练习 3.2 💻 写代码 AFTER UPDATE · 记日志

需求:商品表 product 中价格变化时,记录到 price_log 表。已有日志表如下:

📋 日志表结构

price_log(pid CHAR(10), old_price DECIMAL(10,2), new_price DECIMAL(10,2), change_time DATETIME)

写一个触发器,当商品价格被修改时,自动记录:商品 ID、旧价格、新价格、修改时间。

📖 查看答案

解题思路:

① 要"记日志"(数据已经改了再记)→ 用 AFTER

② 是 修改事件 → UPDATE

③ 既要旧值(OLD.price)又要新值(NEW.price) → UPDATE 触发器都有

SQL · 标准答案
DELIMITER $$

CREATE TRIGGER log_price_change
AFTER UPDATE ON product
FOR EACH ROW
BEGIN
    INSERT INTO price_log VALUES
    (OLD.pid, OLD.price, NEW.price, NOW());
END $$

DELIMITER ;
💡 改进版(只在价格真变化时才记录)

更好的写法:加个 IF 判断,只在 OLD.price 和 NEW.price 不同时才插日志(避免没改价格也记一条):

SQL · 改进版
CREATE TRIGGER log_price_change
AFTER UPDATE ON product
FOR EACH ROW
BEGIN
    IF OLD.price != NEW.price THEN
        INSERT INTO price_log VALUES
        (OLD.pid, OLD.price, NEW.price, NOW());
    END IF;
END
练习 3.3 💻 写代码 BEFORE DELETE · 级联删除

需求:当从 course(课程表)中删除一门课时,自动删除所有学生选这门课的选课记录(在 sc 表中)。

📖 查看答案

解题思路:

删除 course 之前,先去删 sc 中的关联记录(不然外键约束可能导致 course 删不掉)→ 用 BEFORE DELETE

② DELETE 触发器中只有 OLD —— 用 OLD.cno 拿到要删的课程号

SQL · 标准答案
DELIMITER $$

CREATE TRIGGER cascade_delete_sc
BEFORE DELETE ON course
FOR EACH ROW
BEGIN
    DELETE FROM sc WHERE cno = OLD.cno;
END $$

DELIMITER ;
💡 注意点

DELETE 触发器只有 OLD,没有 NEW

OLD.cno 就是被删除那门课的课程号

③ 这种"级联删除"也可以用外键约束的 ON DELETE CASCADE 实现,触发器只是另一种思路

练习 3.4 💻 写代码 事件 · 周期性

需求:创建一个事件 daily_archive每天凌晨 2 点,把订单表 orders 中 30 天前的订单 移动到归档表 orders_archive,然后从 orders 表中删除这些数据。

📖 查看答案

解题思路:

① "每天凌晨 2 点" → EVERY 1 DAY + STARTS '日期 02:00:00'

② 多条 SQL → 用 BEGIN…END 包起来 + DELIMITER

③ "30 天前" 用 NOW() - INTERVAL 30 DAY

SQL · 标准答案
-- 先确保事件调度器开启
SET GLOBAL event_scheduler = ON;

DELIMITER $$

CREATE EVENT daily_archive
ON SCHEDULE EVERY 1 DAY
STARTS '2024-12-21 02:00:00'
DO
BEGIN
    -- 1. 先把 30 天前的订单复制到归档表
    INSERT INTO orders_archive
    SELECT * FROM orders
    WHERE order_time < NOW() - INTERVAL 30 DAY;
    
    -- 2. 然后从原表删除
    DELETE FROM orders
    WHERE order_time < NOW() - INTERVAL 30 DAY;
END $$

DELIMITER ;
⚠️ 三个易错点

调度器必须开启,否则事件不跑

② 多条 SQL 要用 BEGIN...END 包起来

③ 用了 BEGIN...END,就要 DELIMITER 改定界符

💡 STARTS 的作用

不写 STARTS,事件从创建时立即开始周期。写 STARTS 可以指定从某个时间点开始

例如题目要求"每天凌晨 2 点",用 STARTS '2024-12-21 02:00:00' 锚定第一次执行时间,之后每隔 1 天跑一次。

🎯 第四部分 · 综合大题期末重点

本部分把整章知识揉到一起,是 必考综合大题

综合大题 1 ⭐⭐⭐ 期末重点 订单系统综合

某电商订单系统有以下需求,请用触发器和事件实现:

📋 业务需求
  1. 需求 A:插入新订单时,如果 status(订单状态)为空,自动设为 '待付款'
  2. 需求 B:修改订单状态时,自动把变化记录到 order_status_log 表(订单 ID、旧状态、新状态、修改时间)
  3. 需求 C:每天凌晨 1 点,自动把"已完成"超过 90 天的订单状态改为 '已归档'
📖 查看答案

需求 A:BEFORE INSERT 触发器(设默认状态)

SQL · 需求 A
DELIMITER $$

CREATE TRIGGER set_default_status
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
    IF NEW.status IS NULL THEN
        SET NEW.status = '待付款';
    END IF;
END $$

DELIMITER ;

需求 B:AFTER UPDATE 触发器(记日志)

SQL · 需求 B
DELIMITER $$

CREATE TRIGGER log_status_change
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
    -- 只在 status 真的变化时才记日志
    IF OLD.status != NEW.status THEN
        INSERT INTO order_status_log VALUES
        (OLD.order_id, OLD.status, NEW.status, NOW());
    END IF;
END $$

DELIMITER ;

需求 C:周期性事件(自动归档)

SQL · 需求 C
-- 先开启调度器
SET GLOBAL event_scheduler = ON;

CREATE EVENT archive_old_orders
ON SCHEDULE EVERY 1 DAY
STARTS '2024-12-21 01:00:00'
DO
UPDATE orders
SET status = '已归档'
WHERE status = '已完成'
  AND complete_time < NOW() - INTERVAL 90 DAY;
💡 解题要点

① 三个需求分别用了 BEFORE INSERT、AFTER UPDATE、EVENT —— 涵盖本章核心知识点

② 数据校验用 BEFORE,记日志用 AFTER,定时任务用 EVENT —— 这是必考的"选型思维"

③ 注意 需求 C 触发的 UPDATE 也会触发需求 B 的触发器,每条归档操作也会记日志 —— 这是连锁效应的经典场景

⚠️ 思考

需求 C 的事件触发了大量 UPDATE,每条都会触发需求 B 的触发器记日志。如果订单数巨大,可能导致 性能问题或日志表爆炸。实际生产中要权衡。

📝 复习重点回顾

通过本次练习,你应该已经:

⭐ 期末考点排行榜

按出现频率排序:

  1. 写触发器代码(综合大题,分值最高)
  2. NEW 和 OLD 在三种操作中的对应(必考)
  3. BEFORE vs AFTER 的选择(应用题)
  4. 触发器 vs 事件 的对比(简答)
  5. AT vs EVERY 的区别(选择)
💡 关键速记

"插入用 NEW,删除用 OLD,修改两个都能用" —— NEW/OLD 对应

"先校验用 BEFORE,后记录用 AFTER" —— 触发时机选择

"AT = 在某时(一次),EVERY = 每隔(周期)" —— 事件调度

"触发器响应数据变化,事件响应时间到来" —— 选型口诀

"事件不跑?先看调度器开了没" —— 必查