存储过程和触发器是 MySQL 的”自动化”能力——把业务逻辑下沉到数据库层,减少应用与数据库之间的往返。
本章示例表:为了让每段代码都能直接运行,本章统一使用以下表结构。先执行一遍再往后读:
CREATE DATABASE IF NOT EXISTS demo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;USE demo;CREATE TABLE IF NOT EXISTS students ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, class_id INT, score INT);CREATE TABLE IF NOT EXISTS student_logs ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT, action VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP);INSERT INTO students (name, class_id, score) VALUES ('张三', 1, 85), ('李四', 1, 92), ('王五', 2, 77);
一、存储过程
1.1 什么是存储过程
存储过程是预编译的 SQL 语句集合,存储在数据库中,通过 CALL 调用。类似 C 语言的函数:有参数、有逻辑、能返回结果。
-- 最简单的存储过程:查询学生数量DELIMITER //CREATE PROCEDURE get_student_count()BEGIN SELECT COUNT(*) AS total FROM students;END //DELIMITER ;-- 调用CALL get_student_count();
DELIMITER //CREATE PROCEDURE find_by_class( IN p_class_id INT, -- 输入:班级ID OUT p_count INT -- 输出:该班人数)BEGIN SELECT COUNT(*) INTO p_count FROM students WHERE class_id = p_class_id;END //DELIMITER ;-- 调用CALL find_by_class(3, @cnt);SELECT @cnt AS class_3_count;
1.3 变量与流程控制
DELIMITER //CREATE PROCEDURE classify_score(IN p_score INT)BEGIN DECLARE v_grade CHAR(1); -- 声明变量 IF p_score >= 90 THEN SET v_grade = 'A'; ELSEIF p_score >= 80 THEN SET v_grade = 'B'; ELSEIF p_score >= 60 THEN SET v_grade = 'C'; ELSE SET v_grade = 'F'; END IF; SELECT v_grade AS grade;END //DELIMITER ;CALL classify_score(85); -- 返回 B
1.4 循环
DELIMITER //CREATE PROCEDURE generate_numbers(IN n INT)BEGIN DECLARE i INT DEFAULT 1; DECLARE result TEXT DEFAULT ''; WHILE i <= n DO SET result = CONCAT(result, i, ' '); SET i = i + 1; END WHILE; SELECT result;END //DELIMITER ;CALL generate_numbers(5); -- 返回 "1 2 3 4 5"
1.5 游标(Cursor)
游标允许逐行处理查询结果,类似 C 的 for 循环遍历数组:
DELIMITER //CREATE PROCEDURE update_all_scores()BEGIN DECLARE v_id INT; DECLARE v_score INT; DECLARE v_done INT DEFAULT 0; DECLARE cur CURSOR FOR SELECT id, score FROM students; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_score; IF v_done THEN LEAVE read_loop; END IF; -- 对每个学生的分数加 5 分(不超过 100) UPDATE students SET score = LEAST(v_score + 5, 100) WHERE id = v_id; END LOOP; CLOSE cur;END //DELIMITER ;
1.6 存储过程 vs 应用代码
维度
存储过程
应用代码 (Java/Python)
性能
预编译,减少网络往返
每次发送完整 SQL
维护
分散在数据库中,版本控制困难
集中在代码仓库,Git 管理
测试
难以单元测试
容易测试
移植
绑定特定数据库方言
数据库无关(ORM)
适用场景
批量数据处理、ETL、报表
业务逻辑、CRUD
建议:简单查询和批量操作可以用存储过程,复杂业务逻辑放在应用层。
二、触发器
触发器是自动执行的 SQL 代码,在 INSERT/UPDATE/DELETE 操作前后触发。
2.1 基本语法
-- 日志表已在章首创建(student_logs),此处直接建触发器-- 创建触发器:插入学生时自动记录日志DELIMITER //CREATE TRIGGER student_after_insertAFTER INSERT ON studentsFOR EACH ROWBEGIN INSERT INTO student_logs (student_id, action, created_at) VALUES (NEW.id, 'INSERT', NOW());END //DELIMITER ;
2.2 触发器类型
类型
触发时机
用途
BEFORE INSERT
插入前
数据校验、默认值填充
AFTER INSERT
插入后
日志记录、级联更新
BEFORE UPDATE
更新前
数据校验、旧值备份
AFTER UPDATE
更新后
日志记录、统计更新
BEFORE DELETE
删除前
数据备份、软删除
AFTER DELETE
删除后
日志记录、级联清理
2.3 NEW 和 OLD
操作
OLD
NEW
INSERT
不存在
新插入的行
UPDATE
更新前的行
更新后的行
DELETE
被删除的行
不存在
-- 更新前校验分数范围DELIMITER //CREATE TRIGGER student_before_updateBEFORE UPDATE ON studentsFOR EACH ROWBEGIN IF NEW.score < 0 OR NEW.score > 100 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Score must be between 0 and 100'; END IF;END //DELIMITER ;
2.4 触发器 vs 应用代码
维度
触发器
应用代码
执行位置
数据库内部
应用程序
性能
无网络开销
有网络开销
可见性
隐式执行,调试困难
显式调用,易于调试
适用场景
审计日志、数据一致性
业务逻辑
建议:触发器适合做审计日志和数据完整性约束,复杂业务逻辑放在应用层。
三、事件调度器
MySQL 内置的定时任务系统,类似 Linux 的 cron:
-- 每天凌晨 2 点清理 30 天前的日志DELIMITER //CREATE EVENT clean_old_logsON SCHEDULE EVERY 1 DAYSTARTS (TIMESTAMP(CURRENT_DATE) + INTERVAL 1 DAY + INTERVAL 2 HOUR) -- 明天凌晨 2 点开始DOBEGIN DELETE FROM student_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);END //DELIMITER ;-- 查看事件SHOW EVENTS;-- 启用事件调度器SET GLOBAL event_scheduler = ON;
四、实战:学生成绩管理系统
-- 1. 创建日志表CREATE TABLE score_audit ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT, old_score INT, new_score INT, changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);-- 2. 创建触发器:分数变更时记录DELIMITER //CREATE TRIGGER score_change_auditAFTER UPDATE ON studentsFOR EACH ROWBEGIN IF OLD.score != NEW.score THEN INSERT INTO score_audit (student_id, old_score, new_score) VALUES (OLD.id, OLD.score, NEW.score); END IF;END //DELIMITER ;-- 3. 创建存储过程:批量调整分数DELIMITER //CREATE PROCEDURE adjust_scores(IN p_class_id INT, IN p_adjustment INT)BEGIN UPDATE students SET score = LEAST(GREATEST(score + p_adjustment, 0), 100) WHERE class_id = p_class_id; SELECT ROW_COUNT() AS affected_rows;END //DELIMITER ;-- 4. 调用CALL adjust_scores(3, 5); -- 3班全体加5分