1. 实验概述与目标
存储过程和触发器是数据库系统中两个重要的编程对象,它们能够将业务逻辑封装在数据库层面,提高数据操作的效率和安全性。本次实验主要围绕MySQL数据库展开,通过实际案例掌握存储过程和触发器的创建、调用与管理方法。
存储过程(Stored Procedure)是一组预编译的SQL语句集合,它可以接受参数、执行逻辑判断和循环操作,并返回结果。相比直接在应用程序中拼接SQL语句,存储过程具有以下优势:
- 执行效率高:预编译后存储在数据库中
- 网络传输量小:只需传递过程名和参数
- 安全性好:可精细控制权限
- 业务逻辑集中:便于维护和修改
触发器(Trigger)是一种特殊的存储过程,它在特定事件(INSERT/UPDATE/DELETE)发生时自动执行。触发器常用于实现数据完整性约束、审计日志、数据同步等需求。
2. 实验环境准备
2.1 数据库环境配置
实验采用MySQL 8.0作为数据库平台,以下是环境准备步骤:
- 安装MySQL服务器和客户端工具(如MySQL Workbench)
- 创建实验数据库:
CREATE DATABASE db_experiment; USE db_experiment;- 准备测试数据表:
-- 学生表 CREATE TABLE students ( student_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, gender CHAR(1), age INT, department VARCHAR(50) ); -- 课程表 CREATE TABLE courses ( course_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, credit INT DEFAULT 1 ); -- 选课记录表 CREATE TABLE course_selection ( selection_id INT AUTO_INCREMENT PRIMARY KEY, student_id INT, course_id INT, selection_date DATETIME DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,2), FOREIGN KEY (student_id) REFERENCES students(student_id), FOREIGN KEY (course_id) REFERENCES courses(course_id) ); -- 学生成绩历史表(用于触发器演示) CREATE TABLE score_history ( history_id INT AUTO_INCREMENT PRIMARY KEY, selection_id INT, old_score DECIMAL(5,2), new_score DECIMAL(5,2), change_time DATETIME DEFAULT CURRENT_TIMESTAMP, operator VARCHAR(50) );2.2 客户端工具配置
推荐使用MySQL Workbench进行实验,需要注意以下配置:
- 确保连接参数正确(主机名、端口、用户名、密码)
- 设置SQL编辑器参数:
- 取消"Safe Updates"选项(允许UPDATE/DELETE不带WHERE条件)
- 设置默认字符集为utf8mb4
- 对于存储过程调试,建议开启General Log:
SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';注意:实验完成后请关闭General Log以避免性能影响:
SET GLOBAL general_log = 'OFF';
3. 存储过程实验
3.1 基本存储过程创建与调用
3.1.1 创建简单存储过程
创建一个根据学号查询学生信息的存储过程:
DELIMITER // CREATE PROCEDURE get_student_info(IN sid INT) BEGIN SELECT * FROM students WHERE student_id = sid; END // DELIMITER ;关键点说明:
DELIMITER //临时修改语句分隔符,避免与过程中的分号冲突IN表示输入参数,还有OUT(输出)和INOUT(输入输出)类型- 过程体包含在
BEGIN...END块中
调用方法:
CALL get_student_info(1001);3.1.2 带输出参数的存储过程
创建一个统计各院系学生人数的存储过程:
DELIMITER // CREATE PROCEDURE count_students_by_dept( OUT total INT, OUT dept_list VARCHAR(1000) ) BEGIN SELECT COUNT(*) INTO total FROM students; SELECT GROUP_CONCAT(DISTINCT department) INTO dept_list FROM students; END // DELIMITER ;调用方法:
CALL count_students_by_dept(@total, @depts); SELECT @total, @depts;3.2 复杂业务逻辑实现
3.2.1 学生选课业务处理
创建一个处理学生选课的存储过程,包含事务处理和错误检查:
DELIMITER // CREATE PROCEDURE enroll_course( IN sid INT, IN cid INT, OUT result VARCHAR(100) ) BEGIN DECLARE student_exists INT DEFAULT 0; DECLARE course_exists INT DEFAULT 0; DECLARE already_enrolled INT DEFAULT 0; DECLARE credit_limit INT DEFAULT 30; DECLARE current_credit INT DEFAULT 0; -- 检查学生是否存在 SELECT COUNT(*) INTO student_exists FROM students WHERE student_id = sid; -- 检查课程是否存在 SELECT COUNT(*) INTO course_exists FROM courses WHERE course_id = cid; -- 检查是否已选该课程 SELECT COUNT(*) INTO already_enrolled FROM course_selection WHERE student_id = sid AND course_id = cid; -- 计算当前已选学分 SELECT IFNULL(SUM(c.credit), 0) INTO current_credit FROM course_selection cs JOIN courses c ON cs.course_id = c.course_id WHERE cs.student_id = sid; -- 业务规则验证 IF student_exists = 0 THEN SET result = 'Error: Student not exists'; ELSEIF course_exists = 0 THEN SET result = 'Error: Course not exists'; ELSEIF already_enrolled > 0 THEN SET result = 'Error: Already enrolled this course'; ELSEIF current_credit + (SELECT credit FROM courses WHERE course_id = cid) > credit_limit THEN SET result = CONCAT('Error: Credit limit exceeded (Max ', credit_limit, ')'); ELSE -- 通过所有检查,执行选课 START TRANSACTION; INSERT INTO course_selection(student_id, course_id) VALUES(sid, cid); SET result = CONCAT('Success: Enrolled course ', cid); COMMIT; END IF; END // DELIMITER ;调用示例:
-- 成功案例 CALL enroll_course(1001, 101, @result); SELECT @result; -- 失败案例(学分超限) CALL enroll_course(1001, 102, @result); SELECT @result;3.2.2 成绩统计分析
创建一个统计课程成绩分布的存储过程:
DELIMITER // CREATE PROCEDURE analyze_course_scores(IN cid INT) BEGIN DECLARE avg_score DECIMAL(5,2); DECLARE max_score DECIMAL(5,2); DECLARE min_score DECIMAL(5,2); DECLARE pass_rate DECIMAL(5,2); DECLARE student_count INT; -- 获取基本统计信息 SELECT AVG(score), MAX(score), MIN(score), COUNT(*) INTO avg_score, max_score, min_score, student_count FROM course_selection WHERE course_id = cid AND score IS NOT NULL; -- 计算及格率(假设60分及格) SELECT ROUND(COUNT(CASE WHEN score >= 60 THEN 1 END) * 100.0 / COUNT(*), 2) INTO pass_rate FROM course_selection WHERE course_id = cid AND score IS NOT NULL; -- 输出结果 SELECT cid AS course_id, (SELECT name FROM courses WHERE course_id = cid) AS course_name, student_count, avg_score AS average_score, max_score AS highest_score, min_score AS lowest_score, pass_rate AS pass_percentage; -- 输出分数段分布 SELECT CASE WHEN score >= 90 THEN 'A (90-100)' WHEN score >= 80 THEN 'B (80-89)' WHEN score >= 70 THEN 'C (70-79)' WHEN score >= 60 THEN 'D (60-69)' ELSE 'F (<60)' END AS grade_band, COUNT(*) AS student_count, ROUND(COUNT(*) * 100.0 / student_count, 2) AS percentage FROM course_selection WHERE course_id = cid AND score IS NOT NULL GROUP BY grade_band ORDER BY grade_band; END // DELIMITER ;调用方法:
CALL analyze_course_scores(101);3.3 存储过程管理
3.3.1 查看存储过程
查看数据库中的所有存储过程:
SHOW PROCEDURE STATUS WHERE Db = 'db_experiment';查看特定存储过程的定义:
SHOW CREATE PROCEDURE get_student_info;3.3.2 修改存储过程
MySQL不支持直接修改存储过程,需要先删除再重建:
DROP PROCEDURE IF EXISTS get_student_info; DELIMITER // CREATE PROCEDURE get_student_info(IN sid INT) BEGIN SELECT s.student_id, s.name, s.department, COUNT(cs.course_id) AS course_count, IFNULL(SUM(c.credit), 0) AS total_credit FROM students s LEFT JOIN course_selection cs ON s.student_id = cs.student_id LEFT JOIN courses c ON cs.course_id = c.course_id WHERE s.student_id = sid GROUP BY s.student_id; END // DELIMITER ;3.3.3 删除存储过程
DROP PROCEDURE IF EXISTS procedure_name;4. 触发器实验
4.1 基本触发器创建
4.1.1 成绩修改审计触发器
创建一个在成绩修改时记录历史记录的触发器:
DELIMITER // CREATE TRIGGER before_score_update BEFORE UPDATE ON course_selection FOR EACH ROW BEGIN IF OLD.score <> NEW.score THEN INSERT INTO score_history( selection_id, old_score, new_score, operator ) VALUES( OLD.selection_id, OLD.score, NEW.score, CURRENT_USER() ); END IF; END // DELIMITER ;测试触发器:
-- 初始成绩 UPDATE course_selection SET score = 85 WHERE selection_id = 1; -- 修改成绩(将触发触发器) UPDATE course_selection SET score = 90 WHERE selection_id = 1; -- 查看审计记录 SELECT * FROM score_history;4.1.2 数据完整性检查触发器
创建一个确保学生年龄合理的触发器:
DELIMITER // CREATE TRIGGER check_student_age BEFORE INSERT ON students FOR EACH ROW BEGIN IF NEW.age < 15 OR NEW.age > 50 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid student age (15-50 only)'; END IF; END // DELIMITER ;测试触发器:
-- 合法数据 INSERT INTO students VALUES(1005, '张三', 'M', 20, '计算机系'); -- 非法数据(将触发错误) INSERT INTO students VALUES(1006, '李四', 'F', 12, '数学系');4.2 高级触发器应用
4.2.1 级联更新触发器
创建一个在院系名称变更时自动更新相关记录的触发器:
DELIMITER // CREATE TRIGGER after_dept_change AFTER UPDATE ON students FOR EACH ROW BEGIN IF OLD.department <> NEW.department THEN -- 记录院系变更历史 INSERT INTO department_change_log SET student_id = NEW.student_id, old_department = OLD.department, new_department = NEW.department, change_time = NOW(); -- 更新相关统计信息 UPDATE department_stats SET student_count = student_count - 1 WHERE department_name = OLD.department; UPDATE department_stats SET student_count = student_count + 1 WHERE department_name = NEW.department; END IF; END // DELIMITER ;4.2.2 复杂业务规则触发器
创建一个检查选课时间冲突的触发器:
DELIMITER // CREATE TRIGGER check_course_schedule BEFORE INSERT ON course_selection FOR EACH ROW BEGIN DECLARE conflict_count INT DEFAULT 0; -- 假设课程有时间安排,检查时间冲突 SELECT COUNT(*) INTO conflict_count FROM course_selection cs JOIN course_schedule s1 ON cs.course_id = s1.course_id JOIN course_schedule s2 ON NEW.course_id = s2.course_id WHERE cs.student_id = NEW.student_id AND cs.course_id <> NEW.course_id AND s1.day_of_week = s2.day_of_week AND s1.start_time < s2.end_time AND s1.end_time > s2.start_time; IF conflict_count > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Course schedule conflict detected'; END IF; END // DELIMITER ;4.3 触发器管理
4.3.1 查看触发器
查看数据库中的所有触发器:
SHOW TRIGGERS FROM db_experiment;查看特定触发器的定义:
SHOW CREATE TRIGGER before_score_update;4.3.2 禁用/启用触发器
MySQL没有直接禁用触发器的语法,可以通过临时删除再重建的方式实现:
-- 禁用触发器 DROP TRIGGER IF EXISTS before_score_update; -- 执行不需要触发器的操作... -- 重新创建触发器 DELIMITER // CREATE TRIGGER before_score_update BEFORE UPDATE ON course_selection FOR EACH ROW BEGIN -- 触发器逻辑 END // DELIMITER ;4.3.3 删除触发器
DROP TRIGGER IF EXISTS trigger_name;5. 常见问题与调试技巧
5.1 存储过程常见错误
语法错误:
- 确保正确使用DELIMITER
- 检查BEGIN/END块匹配
- 确保所有变量都已声明
权限问题:
- 创建存储过程需要CREATE ROUTINE权限
- 执行存储过程需要EXECUTE权限
参数类型不匹配:
- 确保调用时参数类型与定义一致
- 注意NULL值处理
调试方法:
-- 在过程中添加调试输出 DECLARE debug_msg VARCHAR(1000); SET debug_msg = CONCAT('Variable value: ', some_var); SELECT debug_msg; -- 使用SIGNAL输出调试信息 SIGNAL SQLSTATE '01000' SET MESSAGE_TEXT = 'Debug message';5.2 触发器常见问题
无限循环:
- 避免触发器修改其所在的表
- 使用标志变量防止递归触发
性能问题:
- 触发器中的复杂逻辑会影响DML性能
- 考虑使用存储过程代替高频率触发的触发器
事务隔离:
- 触发器在调用者的事务中执行
- 触发器失败会导致整个事务回滚
调试技巧:
-- 创建日志表记录触发器执行 CREATE TABLE trigger_debug_log ( id INT AUTO_INCREMENT PRIMARY KEY, trigger_name VARCHAR(50), message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 在触发器中添加日志 INSERT INTO trigger_debug_log(trigger_name, message) VALUES('trigger_name', CONCAT('Old: ', OLD.column, ' New: ', NEW.column));5.3 性能优化建议
存储过程优化:
- 避免在循环中执行SQL
- 使用临时表存储中间结果
- 合理使用索引提高查询效率
触发器优化:
- 保持触发器逻辑简单
- 避免在触发器中执行耗时操作
- 考虑使用批量操作减少触发次数
通用建议:
- 使用EXPLAIN分析查询计划
- 定期优化表结构
- 监控数据库性能指标
6. 实验总结与扩展
通过本次实验,我们系统掌握了MySQL存储过程和触发器的开发技术。在实际数据库应用中,这些技术可以发挥重要作用:
存储过程最佳实践:
- 使用清晰的命名规范(如sp_前缀)
- 添加充分的注释说明
- 参数验证和错误处理要完备
- 考虑版本控制
触发器使用场景:
- 数据审计和变更追踪
- 复杂业务规则实施
- 派生数据自动维护
- 跨表一致性维护
扩展学习方向:
- 存储函数开发
- 事件调度器使用
- 游标和异常处理
- 动态SQL构建
在实际项目中,应根据具体需求合理使用存储过程和触发器,避免过度使用导致系统难以维护。建议将复杂业务逻辑放在应用层实现,而将数据相关的核心逻辑放在数据库层实现。