这是一节动手课:每个任务都先自己写、自己跑,跑不出来再看提示,最后对参考答案。先看一眼速查区,然后从环境准备开始。👇
📌 动手前 · 30 秒速查
DELIMITER $$
CREATE TRIGGER 触发器名
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON 表名
FOR EACH ROW
BEGIN
-- 触发时执行的 SQL
END $$
DELIMITER ;
最容易踩的三个坑
NEW、删除用 OLD、修改两个都行。BEFORE、想记日志用 AFTER。SET GLOBAL event_scheduler = ON;环境准备:建好练习用的表
🛠️ 第一步:建库、建表、灌数据
直接整段复制到 Navicat / 命令行 / Workbench 运行即可。已自带一个学生(s3)成绩为空,后面任务会用到。
-- 1. 建库并使用
CREATE DATABASE IF NOT EXISTS db_trigger_lab DEFAULT CHARSET utf8mb4;
USE db_trigger_lab;
-- 2. 重新建表(先删后建,方便反复练习)
DROP TABLE IF EXISTS sc;
DROP TABLE IF EXISTS s;
DROP TABLE IF EXISTS c;
CREATE TABLE s ( -- 学生表
sno CHAR(5) PRIMARY KEY, -- 学号
sn VARCHAR(10) NOT NULL, -- 姓名
sex CHAR(2), -- 性别
maj VARCHAR(30) -- 专业
);
CREATE TABLE c ( -- 课程表
cno CHAR(5) PRIMARY KEY, -- 课程号
cn VARCHAR(30) NOT NULL -- 课程名
);
CREATE TABLE sc ( -- 选课成绩表
sno CHAR(5),
cno CHAR(5),
score DECIMAL(5,1), -- 成绩(允许为空)
PRIMARY KEY (sno, cno)
);
-- 3. 灌入测试数据
INSERT INTO s VALUES
('s1','张三','男','信息管理'),
('s2','李四','女','信息管理'),
('s3','王五','男','计算机');
INSERT INTO c VALUES
('c1','数据库原理'),
('c2','高等数学'),
('c3','Python程序设计');
INSERT INTO sc VALUES
('s1','c1',88.0),
('s1','c2',76.5),
('s2','c1',92.0);
🛠️ 第二步:打开事件调度器(拓展任务要用)
SET GLOBAL event_scheduler = ON;
SHOW VARIABLES LIKE 'event_scheduler'; -- 确认显示 ON
左侧能看到 s、c、sc 三张表;执行 SELECT * FROM s; 能看到 3 个学生;调度器显示 ON。三项都对,就可以开始了。
实验一 · 基础任务
在 sc 表上创建一个触发器 trg_score_zero:插入成绩时,如果 score 是空的(NULL),自动改成 0 再存进去。
💡 卡住了?看这一条提示
① 要"改将要写入的值" → 用 BEFORE;是插入 → INSERT。
② 在 BEGIN…END 里写:IF NEW.score IS NULL THEN SET NEW.score = 0; END IF;
③ 别忘了首尾的 DELIMITER $$ 和 DELIMITER ;。
INSERT INTO sc VALUES ('s3', 'c2', NULL); -- 故意插一个空成绩
SELECT * FROM sc WHERE sno='s3' AND cno='c2';
参考答案
DELIMITER $$
CREATE TRIGGER trg_score_zero
BEFORE INSERT ON sc
FOR EACH ROW
BEGIN
IF NEW.score IS NULL THEN
SET NEW.score = 0;
END IF;
END $$
DELIMITER ;
关键就一句 SET NEW.score = 0; —— 在 BEFORE 里改 NEW,真的会改掉写进库的值。
学生改专业时,自动记一条日志:学号、旧专业、新专业、修改时间。先建好日志表,再写触发器 trg_log_maj。
CREATE TABLE maj_log (
sno CHAR(5),
old_maj VARCHAR(30),
new_maj VARCHAR(30),
utime DATETIME
);
💡 卡住了?看这一条提示
① 是"记录已发生的事" → 用 AFTER;是修改 → UPDATE。
② 旧专业 = OLD.maj,新专业 = NEW.maj,时间 = NOW(),一句 INSERT INTO maj_log VALUES(...) 搞定。
UPDATE s SET maj = '软件工程' WHERE sno = 's1';
SELECT * FROM maj_log;
参考答案
DELIMITER $$
CREATE TRIGGER trg_log_maj
AFTER UPDATE ON s
FOR EACH ROW
BEGIN
INSERT INTO maj_log VALUES
(OLD.sno, OLD.maj, NEW.maj, NOW());
END $$
DELIMITER ;
这是 UPDATE 触发器唯一会同时用上 OLD 和 NEW 的经典套路:旧值 + 新值 + 时间,塞进日志表。
实验二 · 提高任务
删除学生表 s 中的某个学生时,自动把他在 sc 表里的所有选课记录也删掉。创建触发器 trg_cascade_del。
💡 提示
删 s 之前先去删 sc → 用 BEFORE DELETE。DELETE 触发器只有 OLD,要删的学生学号就是 OLD.sno,写一句 DELETE FROM sc WHERE sno = OLD.sno; 即可。
SELECT COUNT(*) FROM sc WHERE sno='s1'; -- 删之前:s1 有几条选课
DELETE FROM s WHERE sno='s1'; -- 删掉学生 s1
SELECT COUNT(*) FROM sc WHERE sno='s1'; -- 删之后:应该是 0
参考答案
DELIMITER $$
CREATE TRIGGER trg_cascade_del
BEFORE DELETE ON s
FOR EACH ROW
BEGIN
DELETE FROM sc WHERE sno = OLD.sno;
END $$
DELIMITER ;
记住:DELETE 触发器里只有 OLD,没有 NEW,因为记录马上就要没了,只剩"以前"。
创建一个每 20 秒自动跑一次的事件 evt_heartbeat,每次往一张"心跳表"插入一条带当前时间的记录。等一分钟后查询,你会看到记录在自己增加 —— 这就是事件在自动工作。
CREATE TABLE heartbeat (
id INT AUTO_INCREMENT PRIMARY KEY,
beat_time DATETIME
);
💡 提示
① "每隔一段时间" → 用 EVERY,写 ON SCHEDULE EVERY 20 SECOND。
② 只有一条 SQL,可以直接 DO INSERT INTO heartbeat(beat_time) VALUES (NOW());,不用 BEGIN…END。
SELECT * FROM heartbeat ORDER BY id;
练完执行 DROP EVENT IF EXISTS evt_heartbeat; 把事件删掉。另外:这里用 20 秒只是为了课堂能当场看到效果;真实项目里清理/备份类事件一般是 EVERY 1 DAY 这种长周期。
参考答案
-- 调度器要先开(环境准备里已开)
CREATE EVENT evt_heartbeat
ON SCHEDULE EVERY 20 SECOND
DO
INSERT INTO heartbeat(beat_time) VALUES (NOW());
如果查出来一直只有 0 行,99% 是调度器没开 —— 回到环境准备执行 SET GLOBAL event_scheduler = ON;。
实验三 · 拓展任务
给一个迷你订单系统加两个触发器,让它"自己照看自己"。
CREATE TABLE orders (
oid CHAR(6) PRIMARY KEY,
amount DECIMAL(10,2),
status VARCHAR(10) -- 订单状态,可能为空
);
CREATE TABLE order_log (
oid CHAR(6),
old_status VARCHAR(10),
new_status VARCHAR(10),
ctime DATETIME
);
需求 A:插入订单时,如果 status 为空,自动设为 '待付款'。
需求 B:修改订单状态时,把"订单号 / 旧状态 / 新状态 / 时间"记到 order_log;仅在状态真的变了才记。
💡 提示
需求 A → BEFORE INSERT,套任务 1 的写法,把判断换成 NEW.status IS NULL。
需求 B → AFTER UPDATE,套任务 2 的写法,但外面包一层 IF OLD.status != NEW.status THEN … END IF; 避免没改也记一条。
-- 验证 A:插一条不带 status 的订单
INSERT INTO orders(oid, amount) VALUES ('o1001', 99.00);
SELECT * FROM orders; -- status 应是 '待付款'
-- 验证 B:改状态,看日志
UPDATE orders SET status='已付款' WHERE oid='o1001';
SELECT * FROM order_log; -- 应有一条 待付款→已付款
参考答案
需求 A · BEFORE INSERT
DELIMITER $$
CREATE TRIGGER trg_order_default
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
IF NEW.status IS NULL THEN
SET NEW.status = '待付款';
END IF;
END $$
DELIMITER ;
需求 B · AFTER UPDATE(只在状态变了才记)
DELIMITER $$
CREATE TRIGGER trg_order_log
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
IF OLD.status != NEW.status THEN
INSERT INTO order_log VALUES
(OLD.oid, OLD.status, NEW.status, NOW());
END IF;
END $$
DELIMITER ;
两个触发器各管一段:A 负责"进门时给默认值",B 负责"改动留痕"。这正是讲义里"校验用 BEFORE、记日志用 AFTER"的组合实战。
需求 B 如果去掉 IF OLD.status != NEW.status 这层判断,会发生什么?提示:哪怕你执行一个"啥也没改"的 UPDATE,日志表也会……?
完成检查
完成进度:0 / 6
回顾一下你今天真正动手做出来的:BEFORE/AFTER 触发器、NEW/OLD 取值、级联删除、会自动跑的事件。这四样,就是触发器和事件的全部要点。