【MySQL学习】存储过程
目录
一、定义
二、基本语法
1.创建存储过程
2.删除存储过程
3.查看存储过程
三、控制语句
1.变量声明与赋值
四、游标(Cursor)
(1)声明游标
(2)处理游标结束
(3)打开游标
(4)读取数据
(5)关闭景点
五、案例语句
1.值匹配
2.条件匹配
五、综合案例
第一步:创建表
第二步:创建存储过程:ProcessStudentGrades
第三步:测试
一、定义
存储过程是一组预编译的 SQL 语句集合,经过编译后存储在数据库服务器上。
可以把它想象成数据库中的一个自定义函数或脚本。
主要用于封装常用的、复杂的或重复性的数据库操作,简化应用程序开发,提高性能和安全性。
二、基本语法
1.创建存储过程
DELIMITER //
CREATE PROCEDURE procedure_name ([parameter_list])
BEGIN-- SQL 语句
END //
DELIMITER ;
(1)DELIMITER //
和 DELIMITER ;
存储过程内部通常包含分号 (;
) 作为 SQL 语句的结束符,为了让 MySQL 客户端能够区分存储过程定义的结束 (END
) 和内部 SQL 语句的结束,需要临时改变语句结束符(常用 //
或 $$
),定义完毕后再改回默认的分号 (;
)。
(2) parameter_list:参数列表(可选),可以是输入、输出或输入输出参数。
IN
: 输入参数(默认模式)。值由调用者传入存储过程,在过程中只读。OUT
: 输出参数。值在存储过程中被设置,并可以返回给调用者。调用时传入的变量会被修改。INOUT
: 输入输出参数。调用者传入初始值,存储过程可以读取和修改它,修改后的值可以返回给调用者。
示例1:计算公司给定部门的总人数
DELIMITER //
CREATE PROCEDURE get_employee_count_by_dept(IN dept_id INT, OUT emp_count INT)
BEGINSELECT COUNT(*) INTO emp_countFROM employeesWHERE department_id = dept_id;
END //
DELIMITER ;
CALL get_employee_count_by_dept(10, @count);
SELECT @count;
CALL get_employee_count_by_dept(10, @count); -- 将部门 10 的员工数放入 @count 变量
示例2:给定num,计算num+1的值
CREATE PROCEDURE increment(INOUT num INT)
BEGINSET num = num + 1;
END //
SET @value = 10;
CALL increment(@value);
SELECT @value;
2.删除存储过程
DROP PROCEDURE [IF EXISTS] procedure_name;
3.查看存储过程
SHOW PROCEDURE STATUS [WHERE condition];
示例1:
SHOW PROCEDURE STATUS;
- 说明:没有WHERE子句,启动当前用户有权限访问的所有存储过程的信息。
- 结果:返回所有存储过程的元数据,可能包含多个数据库中的存储过程。
示例二:
SHOW PROCEDURE STATUS WHERE Db = 'AlinJ';
(2)查看存储过程定义
SHOW CREATE PROCEDURE procedure_name;
三、控制语句
1.变量声明与赋值
(1)变量声明
在存储过程中定义局部变量,用于存储临时数据
DECLARE variable_name data_type [DEFAULT value];
- 变量声明必须位于BEGIN ... END块的开头,且位于任何其他语句之前。
- 变量的作用域仅限于当前BEGIN ... END块,块结束后变量自动推理。
(2)变量赋值
方法一:SET语句
SET variable_name = ALinJ;
方法二:SELECT ... INTO 语句
SELECT column_name INTO variable_name FROM table_name WHERE condition;
示例:将employees表中id为1的员工的姓名赋给变量
SELECT name INTO @emp_name FROM employees WHERE id = 1;
SELECT @emp_name;
(3)示例:统计用户表中用户数量,并根据数量进行状态设置。
DELIMITER //
CREATE PROCEDURE count_users()
BEGINDECLARE total_users INT DEFAULT 0;DECLARE user_status VARCHAR(20);SELECT COUNT(*) INTO total_users FROM users;SET user_status = IF(total_users > 100, 'High', 'Low');SELECT total_users AS 'Total Users', user_status AS 'User Status';
END //
DELIMITER ;CALL count_users();
2. 条件语句(IF-ELSE)
IF condition THEN-- 语句
ELSEIF condition THEN-- 语句
ELSE-- 语句
END IF;
示例1:根据用户分数判断是否及格
DELIMITER //
CREATE PROCEDURE check_score(IN score INT, OUT result VARCHAR(20))
BEGINIF score >= 60 THENSET result = 'Pass';ELSESET result = 'Fail';END IF;SELECT result AS 'Result';
END //
DELIMITER ;CALL check_score(75, @result);
示例2:根据分数评定等级(A、B、C)
DELIMITER //
CREATE PROCEDURE grade_score(IN score INT, OUT grade VARCHAR(10))
BEGINIF score >= 90 THENSET grade = 'A';ELSEIF score >= 75 THENSET grade = 'B';ELSEIF score >= 60 THENSET grade = 'C';ELSESET grade = 'Fail';END IF;SELECT grade AS 'Grade';
END //
DELIMITER ;CALL grade_score(85, @grade);
示例3:检查分数是否有效,并评估等级
DELIMITER //
CREATE PROCEDURE validate_score(IN score INT, OUT message VARCHAR(50))
BEGINIF score < 0 OR score > 100 THENSET message = 'Invalid score';ELSEIF score >= 60 THENSET message = 'Pass';ELSESET message = 'Fail';END IF;END IF;SELECT message AS 'Message';
END //
DELIMITER ;CALL validate_score(-10, @message);
3. 循环语句
(1)WHILE 循环
WHILE condition DO-- 语句
END WHILE;
- 条件为真时执行循环体。
- 条件在循环开始前检查,可能一次都不执行。
示例:插入 1 到 5 的数字到表中。
CREATE TABLE numbers (num INT);DELIMITER //
CREATE PROCEDURE insert_numbers()
BEGINDECLARE i INT DEFAULT 1;WHILE i <= 5 DOINSERT INTO numbers VALUES (i);SET i = i + 1;END WHILE;SELECT * FROM numbers;
END //
DELIMITER ;CALL insert_numbers();
(2)REPEAT 循环
REPEAT-- 语句
UNTIL condition
END REPEAT;
- 先执行循环体,再检查条件。
- 至少执行一次,条件为真时退出循环。
DELIMITER //
CREATE PROCEDURE calculate_sum(OUT total INT)
BEGINDECLARE i INT DEFAULT 1;SET total = 0;REPEATSET total = total + i;SET i = i + 1;UNTIL i > 5END REPEAT;SELECT total AS 'Total Sum';
END //
DELIMITER ;CALL calculate_sum(@total);
(3)LOOP 循环
[label:] LOOP-- 语句IF condition THENLEAVE label; -- 相当于breakEND IF;-- ITERATE label; -- 相当于continue
END LOOP [label];
- 无条件循环,必须通过LEAVE语句退出。
- ITERATE语句可跳到下一次循环。
- label是可选的循环标签,用于LEAVE和ITERATE。
示例1:插入 1 到 5 的数字,并跳过 3
DELIMITER //
CREATE PROCEDURE insert_numbers_with_skip()
BEGINDECLARE i INT DEFAULT 1;my_loop: LOOPIF i > 5 THENLEAVE my_loop;END IF;IF i = 3 THENSET i = i + 1;ITERATE my_loop;END IF;INSERT INTO numbers VALUES (i);SET i = i + 1;END LOOP my_loop;SELECT * FROM numbers;
END //
DELIMITER ;CALL insert_numbers_with_skip();
四、游标(Cursor)
游标是 MySQL 存储过程中用于处理查询结果集的工具,允许逐行读取和操作查询结果。
基本使用步骤:
- 声明指标:用于存储游标读取的数据和控制循环的标志。
- 声明游标:绑定一个SELECT语句。
- 声明HANDLER:处理游标读取结束。
- 打开景点。
- 循环读取数据:用FETCH读取每一行,处理逻辑。
- 关闭游标。
(1)声明游标
CLOSE cursor_name;
DECLARE cursor_name CURSOR FOR select_statement;
- 游标必须绑定一个SELECT语句。
- 声明必须在存储过程的BEGIN ... END块开头,在变量声明之后。
(2)处理游标结束
- 问题:当游标读取到最后一行后,继续FETCH会导致错误。
- 解决方法:使用HANDLER处理NOT FOUND条件。
DECLARE CONTINUE HANDLER FOR NOT FOUND SET variable = value;
(3)打开游标
OPEN cursor_name;
- 游标后,MySQL 会执行SELECT语句,并将结果集加载到内存中。
- 此时游标指向结果集的第一行之前。
(4)读取数据
从游标中读取一行数据,将数据存储到指定的变量中。
FETCH cursor_name INTO variable_list;
(5)关闭景点
CLOSE cursor_name;
示例1:遍历用户表,记录每个用户的ID和名称到日志表
第一步:准备数据
CREATE TABLE users (id INT, name VARCHAR(50));
CREATE TABLE logs (log_id INT AUTO_INCREMENT PRIMARY KEY, message VARCHAR(100));INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');
第二步:创建存储过程
DELIMITER //
CREATE PROCEDURE log_users()
BEGIN-- 声明变量DECLARE done INT DEFAULT 0;DECLARE u_id INT;DECLARE u_name VARCHAR(50);-- 声明游标DECLARE cur CURSOR FOR SELECT id, name FROM users;-- 声明 HANDLERDECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;-- 打开游标OPEN cur;-- 循环读取read_loop: LOOPFETCH cur INTO u_id, u_name;IF done THENLEAVE read_loop;END IF;-- 插入日志INSERT INTO logs (message) VALUES (CONCAT('User: ', u_id, ', Name: ', u_name));END LOOP read_loop;-- 关闭游标CLOSE cur;-- 查看结果SELECT * FROM logs;
END //
DELIMITER ;
第三步:调用
CALL log_users();
示例二:遍历用户表,给分数低于60的用户加10分,并记录操作日志。
第一步:准备数据
CREATE TABLE users (id INT, name VARCHAR(50), score INT);
CREATE TABLE logs (log_id INT AUTO_INCREMENT PRIMARY KEY, message VARCHAR(100), log_time DATETIME);INSERT INTO users VALUES (1, 'Alice', 50), (2, 'Bob', 70), (3, 'Charlie', 45);
第二步:创建存储过程
DELIMITER //
CREATE PROCEDURE update_scores()
BEGINDECLARE done INT DEFAULT 0;DECLARE u_id INT;DECLARE u_score INT;DECLARE cur CURSOR FOR SELECT id, score FROM users;DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;OPEN cur;update_loop: LOOPFETCH cur INTO u_id, u_score;IF done THENLEAVE update_loop;END IF;-- 检查分数是否低于 60IF u_score < 60 THENSET u_score = u_score + 10;UPDATE users SET score = u_score WHERE id = u_id;INSERT INTO logs (message, log_time) VALUES (CONCAT('User ', u_id, ' score updated to ', u_score), NOW());END IF;END LOOP update_loop;CLOSE cur;SELECT * FROM users;SELECT * FROM logs;
END //
DELIMITER ;
第三步:调用
CALL update_scores();
示例3:将users表中的数据迁移到users_backup表,跳过分数低于50的用户。
第一步:准备数据
CREATE TABLE users (id INT, name VARCHAR(50), score INT);
CREATE TABLE users_backup (id INT, name VARCHAR(50), score INT);INSERT INTO users VALUES (1, 'Alice', 50), (2, 'Bob', 70), (3, 'Charlie', 40);
第二步:创建存储过程
DELIMITER //
CREATE PROCEDURE backup_users()
BEGINDECLARE done INT DEFAULT 0;DECLARE u_id INT;DECLARE u_name VARCHAR(50);DECLARE u_score INT;DECLARE cur CURSOR FOR SELECT id, name, score FROM users;DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;OPEN cur;backup_loop: LOOPFETCH cur INTO u_id, u_name, u_score;IF done THENLEAVE backup_loop;END IF;-- 跳过分数低于 50 的用户IF u_score < 50 THENITERATE backup_loop;END IF;-- 插入到备份表INSERT INTO users_backup VALUES (u_id, u_name, u_score);END LOOP backup_loop;CLOSE cur;SELECT * FROM users_backup;
END //
DELIMITER ;
第三步:调用
CALL backup_users();
五、案例语句
1.值匹配
CASE expressionWHEN value1 THEN-- 语句WHEN value2 THEN-- 语句[ELSE-- 语句]
END CASE;
示例:根据用户等级设置描述。
DELIMITER //
CREATE PROCEDURE describe_level(IN level CHAR(1), OUT description VARCHAR(20))
BEGINCASE levelWHEN 'A' THENSET description = 'Excellent';WHEN 'B' THENSET description = 'Good';WHEN 'C' THENSET description = 'Average';ELSESET description = 'Unknown';END CASE;SELECT description AS 'Description';
END //
DELIMITER ;CALL describe_level('B', @description);
2.条件匹配
CASEWHEN condition1 THEN-- 语句WHEN condition2 THEN-- 语句[ELSE-- 语句]
END CASE;
示例:根据分数评定等级。
DELIMITER //
CREATE PROCEDURE grade_score_case(IN score INT, OUT grade VARCHAR(10))
BEGINCASEWHEN score >= 90 THENSET grade = 'A';WHEN score >= 75 THENSET grade = 'B';WHEN score >= 60 THENSET grade = 'C';ELSESET grade = 'Fail';END CASE;SELECT grade AS 'Grade';
END //
DELIMITER ;CALL grade_score_case(85, @grade);
五、综合案例
我将创建一个存储过程ProcessStudentGrades,用于管理学生的成绩数据,支持以下功能:
- 批量更新成绩(加分或减分)。
- 根据成绩评定等级(A、B、C、Fail)。
- 记录操作日志(例如,谁的成绩被更新、等级如何)。
- 如果操作失败,记录错误日志。
第一步:创建表
CREATE TABLE students (id INT PRIMARY KEY,name VARCHAR(50),grade INT
);CREATE TABLE logs (log_id INT AUTO_INCREMENT PRIMARY KEY,message VARCHAR(255),log_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);INSERT INTO students (id, name, grade)
VALUES (1, 'Alice', 85),(2, 'Bob', 55),(3, 'Charlie', 92);
第二步:创建存储过程:ProcessStudentGrades
DELIMITER //CREATE PROCEDURE ProcessStudentGrades(IN action VARCHAR(20), -- 操作类型:ADD(加分)、SUBTRACT(减分)、GRADE(评定等级)IN score_change INT, -- 分数变化值(加分或减分)OUT result_message VARCHAR(100)
)
BEGIN-- 声明变量DECLARE done INT DEFAULT 0;DECLARE s_id INT;DECLARE s_name VARCHAR(50);DECLARE s_grade INT;DECLARE new_grade INT;DECLARE grade_level VARCHAR(10);DECLARE loop_counter INT DEFAULT 0;-- 声明游标DECLARE cur CURSOR FOR SELECT id, name, grade FROM students;-- 声明游标结束处理器DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;-- 声明错误处理器DECLARE EXIT HANDLER FOR SQLEXCEPTIONBEGINSET result_message = 'Error occurred during operation.';INSERT INTO logs (message) VALUES (CONCAT('Error: ', result_message, ' during action: ', action));END;SET result_message = 'Operation completed successfully.';-- 使用 CASE 语句处理操作类型CASE UPPER(action)WHEN 'ADD' THEN-- 使用游标遍历学生,加分OPEN cur;add_loop: LOOPFETCH cur INTO s_id, s_name, s_grade;IF done THENLEAVE add_loop;END IF;-- 计算新分数SET new_grade = s_grade + score_change;-- 确保分数在 0-100 范围内IF new_grade > 100 THENSET new_grade = 100;END IF;-- 更新分数UPDATE students SET grade = new_grade WHERE id = s_id;INSERT INTO logs (message) VALUES (CONCAT('Student ', s_name, ' (ID: ', s_id, ') grade updated from ', s_grade, ' to ', new_grade));END LOOP add_loop;CLOSE cur;WHEN 'SUBTRACT' THEN-- 使用 WHILE 循环遍历学生,减分SET done = 0; OPEN cur;WHILE NOT done DOFETCH cur INTO s_id, s_name, s_grade;IF done THENLEAVE;END IF;-- 计算新分数SET new_grade = s_grade - score_change;-- 确保分数在 0-100 范围内IF new_grade < 0 THENSET new_grade = 0;END IF;-- 更新分数UPDATE students SET grade = new_grade WHERE id = s_id;INSERT INTO logs (message) VALUES (CONCAT('Student ', s_name, ' (ID: ', s_id, ') grade updated from ', s_grade, ' to ', new_grade));END WHILE;CLOSE cur;WHEN 'GRADE' THEN-- 使用 REPEAT 循环遍历学生,评定等级SET done = 0; OPEN cur;REPEATFETCH cur INTO s_id, s_name, s_grade;IF NOT done THEN-- 使用 IF-ELSE 评定等级IF s_grade >= 90 THENSET grade_level = 'A';ELSEIF s_grade >= 75 THENSET grade_level = 'B';ELSEIF s_grade >= 60 THENSET grade_level = 'C';ELSESET grade_level = 'Fail';END IF;-- 记录等级日志INSERT INTO logs (message) VALUES (CONCAT('Student ', s_name, ' (ID: ', s_id, ') grade: ', s_grade, ', level: ', grade_level));-- 简单计数SET loop_counter = loop_counter + 1;END IF;UNTIL doneEND REPEAT;CLOSE cur;SET result_message = CONCAT('Graded ', loop_counter, ' students successfully.');ELSESIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid action specified.';END CASE;
END //DELIMITER ;
第三步:测试
-- 加分操作
CALL ProcessStudentGrades('ADD', 10, @msg);
SELECT @msg AS Result;
SELECT * FROM students;
SELECT * FROM logs;-- 减分操作
CALL ProcessStudentGrades('SUBTRACT', 5, @msg);
SELECT @msg AS Result;
SELECT * FROM students;
SELECT * FROM logs;-- 评定等级
CALL ProcessStudentGrades('GRADE', NULL, @msg);
SELECT @msg AS Result;
SELECT * FROM logs;-- 无效操作
CALL ProcessStudentGrades('INVALID', NULL, @msg);
SELECT @msg AS Result;
SELECT * FROM logs;