存储过程
1. 什么是存储过程
- 版本支持:MySQL 5.0 及以上版本开始支持。
- 本质定义 :一组预先编译的 SQL 语句集合,是数据库层面的代码封装与复用,功能类似 Java 中的方法 / Python 中的函数。
- 核心作用:可以实现复杂的业务逻辑,将多条 SQL 或流程控制封装为一个可调用的单元。
2. 核心特性
| 特性 | 详细说明 |
|---|---|
| 参数与变量 | 支持输入(IN)、输出(OUT)、输入输出(INOUT)参数,可自定义变量存储中间结果。 |
| 流程控制 | 支持 IF/ELSE、CASE、WHILE、LOOP 等控制语句,能实现分支、循环等复杂逻辑。 |
| 模块化复用 | 像函数一样封装业务逻辑,一次定义、多次调用,提升代码可维护性。 |
| 执行高效 | 首次执行时完成编译与优化,后续调用直接执行,避免重复解析 SQL,性能更优。 |
| 安全可控 | 可通过权限管理限制用户直接操作表,仅开放存储过程调用,降低数据风险。 |
把存储过程想象成 "数据库里的自定义函数 / 工具包":你把一段复杂的业务逻辑(比如 "计算员工年终奖 + 更新绩效")打包成一个 "工具";之后只需要调用这个工具的名字,传入参数,就能自动完成所有操作,不用每次都写一遍完整 SQL。
3. 与视图 / 普通 SQL 的区别
- vs 视图 :视图本质是查询语句封装,无流程控制、无参数;存储过程支持复杂逻辑、参数和变量,功能更强。
- vs 普通 SQL:普通 SQL 单次执行,无法复用逻辑;存储过程可重复调用,且性能更优。
一句话总结:存储过程是 MySQL 中封装复杂业务逻辑的 "可编程 SQL 单元" ,兼具代码复用、性能优化、逻辑封装三大核心优势。
数据准备:
sql
create database mydb7_procedure;
use mydb7_procedure;
CREATE TABLE IF NOT EXISTS dept (
deptno INT PRIMARY KEY, -- 部门编号
dname VARCHAR(20), -- 部门名称
loc VARCHAR(20) -- 部门位置
);
-- 插入数据
INSERT INTO dept (deptno, dname, loc) VALUES
(10, '教研部', '北京'),
(20, '学工部', '上海'),
(30, '销售部', '广州'),
(40, '财务部', '武汉');
CREATE TABLE IF NOT EXISTS emp (
empno INT PRIMARY KEY, -- 员工编号
ename VARCHAR(20), -- 员工姓名
job VARCHAR(20), -- 职位
mgr INT, -- 上级领导编号
hiredate DATE, -- 入职日期
sal DECIMAL(10,2), -- 薪资
comm DECIMAL(10,2), -- 奖金
deptno INT, -- 所属部门编号
FOREIGN KEY (deptno) REFERENCES dept(deptno)
);
-- 插入数据
INSERT INTO emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) VALUES
(1001, '甘宁', '文员', 1013, '2000-12-17', 8000.00, NULL, 20),
(1002, '黛绮丝', '销售员', 1006, '2001-02-20', 16000.00, 3000.00, 30),
(1003, '殷天正', '销售员', 1006, '2001-02-22', 12500.00, 5000.00, 30),
(1004, '刘备', '经理', 1009, '2001-04-02', 29750.00, NULL, 20),
(1005, '谢逊', '销售员', 1006, '2001-09-28', 12500.00, 14000.00, 30),
(1006, '关羽', '经理', 1009, '2001-05-01', 28500.00, NULL, 30),
(1007, '张飞', '经理', 1009, '2001-09-01', 24500.00, NULL, 10),
(1008, '诸葛亮', '分析师', 1004, '2007-04-19', 30000.00, NULL, 20),
(1009, '曾阿牛', '董事长', NULL, '2001-11-17', 50000.00, NULL, 10),
(1010, '韦一笑', '销售员', 1006, '2001-09-08', 15000.00, 0.00, 30),
(1011, '周泰', '文员', 1008, '2007-05-23', 11000.00, NULL, 20),
(1012, '程普', '文员', 1006, '2001-12-03', 9500.00, NULL, 30),
(1013, '庞统', '分析师', 1004, '2001-12-03', 30000.00, NULL, 20),
(1014, '黄盖', '文员', 1007, '2002-01-23', 13000.00, NULL, 10);
CREATE TABLE IF NOT EXISTS salgrade (
grade INT PRIMARY KEY, -- 薪资等级
losal INT, -- 该等级最低薪资
hisal INT -- 该等级最高薪资
);
-- 插入数据
INSERT INTO salgrade (grade, losal, hisal) VALUES
(1, 7000, 12000),
(2, 12010, 14000),
(3, 14010, 20000),
(4, 20010, 30000),
(5, 30010, 99990);
4. 存储过程操作
4.1 创建 / 调用
语法格式:
sql
-- 1. 临时修改语句结束符(避免与存储过程内的 ; 冲突)
DELIMITER //
-- 2. 创建存储过程
CREATE PROCEDURE 存储过程名(
[IN 参数名 数据类型, ] -- 输入参数(默认可省略 IN)
[OUT 参数名 数据类型, ] -- 输出参数
[INOUT 参数名 数据类型] -- 输入输出参数
)
BEGIN
-- 存储过程主体:SQL语句、变量、流程控制等
SELECT * FROM emp;
END //
-- 3. 恢复默认结束符为 ;
DELIMITER ;
关键字解析:
| 语法元素 | 作用说明 |
|---|---|
DELIMITER // |
临时将语句结束符从 ; 改为 //,防止存储过程内部的 ; 被提前解析。 |
CREATE PROCEDURE |
定义存储过程的关键字,后接存储过程名。 |
IN / OUT / INOUT |
IN:输入参数,向存储过程传值(默认类型,可省略) OUT:输出参数,存储过程向外返回结果 INOUT:输入输出参数,既传值又返回结果 |
BEGIN ... END |
包裹存储过程的执行逻辑,相当于代码块。 |
END // |
用之前定义的结束符 // 来标记存储过程的结束。 |
举例:
sql
-- 创建存储过程
delimiter //
CREATE PROCEDURE proc01()
BEGIN
SELECT
*
FROM
emp;
END //
delimiter;
-- 调用存储过程
CALL proc01();
4.2 变量定义
4.2.1 局部变量
语法格式:
sql
declare 变量名 数据类型[default 默认值];
关键字解析:
| 语法元素 | 含义 |
|---|---|
DECLARE |
声明局部变量的关键字,必须写在 BEGIN...END 最开头(在任何 SQL 语句之前)。 |
变量名 |
自定义变量名,建议见名知意(如 emp_count、total_sal),避免与表字段重名。 |
数据类型 |
与 MySQL 字段类型一致,如 INT、VARCHAR(n)、DECIMAL(10,2)、DATE 等。 |
DEFAULT |
可选,为变量设置初始默认值;若不写,变量初始值为 NULL。 |
举例:
sql
-- 给变量赋值
delimiter //
CREATE PROCEDURE proc02 ()
BEGIN
DECLARE var_name VARCHAR (20) DEFAULT('小三');
SET var_name = '张三';
SELECT var_name;
END //
delimiter;
-- 调用存储过程
CALL proc02 ();
-- 方式二 select into
delimiter //
CREATE PROCEDURE proc03 () BEGIN
DECLARE var_name VARCHAR (20);
SELECT ename INTO var_name FROM emp WHERE empno = '1001';
SELECT var_name;
END //
delimiter;
CALL proc03 ();
4.2.2 用户变量
核心概念:
- 用户变量 :由用户自定义,在当前数据库连接(会话)内全程有效,断开连接后自动销毁。
- 类比理解 :类似 Java 中的成员变量,作用域是整个会话,可在多条 SQL、存储过程之间共享数据。
- 关键特性 :无需提前声明,使用时自动创建,非常灵活。
语法格式:
sql
-- 方式一
set @变量名 = 值;
-- 方式二
set @变量名 := 值;
举例:
sql
-- 1. 定义用户变量,存储部门10的平均薪资
SET @deptno = 10;
SELECT @deptno;
-- 2. 查询并赋值给用户变量
SELECT @avg_sal := AVG(sal) from emp where deptno = @deptno;
SELECT @avg_sal;
-- 3. 在后续查询中直接使用该变量
select ename,sal from emp where deptno = @deptno AND sal > @avg_sal;
4.2.3 系统变量
系统变量由 MySQL 内置,分为两类:
- 全局变量(
@@global.变量名):作用于整个 MySQL 服务器,影响所有连接。 - 会话变量(
@@session.变量名/@@变量名):仅作用于当前数据库连接(会话),不影响其他连接。
初始化规则:
1. 全局变量:
- MySQL 启动时自动初始化默认值;
- 可通过修改配置文件
my.ini(Windows)或my.cnf(Linux)来永久改变默认值。
- 会话变量:
- 每次新建连接时,MySQL 会复制一份当前全局变量的值,作为本次会话的初始值;
- 若未手动修改,会话变量与全局变量的值完全一致。
区别:
| 维度 | 全局变量 (@@global.) |
会话变量 (@@session. / @@) |
|---|---|---|
| 作用范围 | 整个 MySQL 服务器,所有连接 | 仅当前连接(会话) |
| 修改影响 | 影响后续所有新连接 | 仅影响当前连接 |
| 生命周期 | MySQL 重启后恢复为配置文件值 | 连接断开后销毁,下次连接重新初始化 |
总结:
- 全局变量 = 服务器级配置,改一次影响所有人;
- 会话变量 = 连接级配置,只改自己当前的连接;
- 新连接诞生时,会 "抄一份" 全局变量当自己的初始会话变量。
4.2.3.1 全局变量
语法格式:
sql
@@global.变量名
举例:
sql
-- 查看全局变量
SHOW global variables;
-- 查看某个全局变量
SELECT @@global.password_history;
-- 修改全局变量的值
SET GLOBAL sort_buffer_size = 40000;
-- 或
SET @@global.sort_buffer_size = 30000;
SELECT @@GLOBAL.sort_buffer_size;
4.2.3.2 会话变量
语法格式:
sql
@@session.变量名;
举例:
sql
-- 查看会话变量
SHOW SESSION variables;
-- 查看某个会话变量
SELECT @@SESSION.sort_buffer_size;
-- 修改全局变量的值
SET SESSION sort_buffer_size = 40000;
-- 或
SET @@session.sort_buffer_size = 30000;
SELECT @@session.sort_buffer_size;
注意:
- 只读变量 :部分系统变量(如
@@version、@@datadir)是只读的,无法用SET修改。 - 修改时机 :修改全局变量不会影响已建立的连接,只会影响后续新建的连接;修改会话变量立即生效。
- 持久化 :
SET @@global.修改的全局变量在 MySQL 重启后会失效,若要永久生效,必须修改配置文件并重启服务。
4.3 参数传递
4.3.1 in
IN 是输入参数 ,用于向存储过程传入数据:
- 可以传入常量值 或变量;
- 传入的变量在存储过程外部不会被修改(值传递);
- 存储过程内部可以修改该参数的值,但仅在过程内部生效,不影响外部原变量。
类比 Java:
IN参数就像方法的值传递参数,方法内部改参数值,不会影响调用方的原变量。类比 Python:函数中的形参。
语法格式:
sql
DELIMITER //
CREATE PROCEDURE 存储过程名(IN 参数名 数据类型)
BEGIN
-- 内部可以使用/修改该参数,但不会影响外部变量
SELECT 参数名;
END //
DELIMITER ;
举例:
sql
delimiter //
create PROCEDURE proc04(IN param_empno int)
BEGIN
SELECT * FROM emp WHERE empno = param_empno;
END //
delimiter;
CALL proc04(1001);
-- 封装有参数的存储过程,可以通过传入部门名和薪资,查询指定部门,并且薪资大于指定值的员工信息
delimiter //
create PROCEDURE proc05(in param_dname varchar(20),in param_sal FLOAT)
BEGIN
select * from dept d inner join emp e on d.deptno = e.deptno where d.dname = param_dname and e.sal > param_sal;
END //
delimiter;
call proc05('学工部',2000);
4.3.2 out
OUT 是输出参数 ,用于从存储过程内部向调用者回传数据:
- 作用:只出不进,负责把存储过程内部计算的值 / 查询结果带出过程;
- 调用时必须传入用户变量 (带
@前缀),用于接收返回值; - 存储过程内部通过
SELECT ... INTO为其赋值。
类比 Java 和 Python:方法 / 函数 最后返回的关键字return。
语法格式:
sql
USE 数据库名; -- 切换到目标库
-- 1. 修改结束符,避免与内部 ; 冲突
DELIMITER $$
-- 2. 创建存储过程,声明 IN 和 OUT 参数
CREATE PROCEDURE 存储过程名(
IN 参数名 数据类型, -- 输入参数
OUT 参数名 数据类型 -- 输出参数(用于接收返回值)
)
BEGIN
-- 3. 核心逻辑:将查询结果或计算值赋值给 OUT 参数
SELECT 目标字段 INTO 参数 FROM 表 WHERE 条件;
END $$
-- 4. 恢复默认结束符
DELIMITER ;
-- 5. 调用并接收结果
CALL 存储过程名(传入值, @接收变量);
SELECT @接收变量; -- 查看输出结果
举例:
sql
-- 封装有参数的存储过程,传入员工编号,返回员工名字
delimiter //
create procedure proc07(in param_empno int,out param_name VARCHAR(20),out param_sal DECIMAL(7,2))
BEGIN
select ename,sal into param_name,param_sal from emp where empno = param_empno;
END //
delimiter;
CALL proc07(1001,@param_name,@param_sal);
SELECT @param_name,@param_sal;
4.3.3 inout
INOUT 是输入输出参数 ,兼具 IN 和 OUT 的特性:
- 既进又出:先从外部传入初始值,在存储过程内部修改后,再将新值返回给外部调用者;
- 传入的变量会被直接修改,过程执行结束后,外部变量的值会同步更新为过程内修改后的值;
- 类比 Java:类似引用传递,方法内部修改参数会直接影响外部原变量。
语法格式:
sql
DELIMITER $$
CREATE PROCEDURE 存储过程名(
INOUT 参数名 数据类型 -- 声明为输入输出参数
)
BEGIN
-- 1. 可以直接使用传入的初始值
-- 2. 在过程内部修改参数值
SET 参数名 = 新值;
END $$
DELIMITER ;
-- 调用:必须传入用户变量 @xxx,初始值会被修改
SET @变量名 = 初始值;
CALL 存储过程名(@变量名);
SELECT @变量名; -- 查看修改后的值
举例:
sql
-- 传入员工名,拼接部门号,传入薪资,求出年薪
delimiter //
CREATE PROCEDURE proc08(
INOUT param_ename VARCHAR(20),
INOUT param_sal DECIMAL(10,2)
)
BEGIN
SELECT CONCAT(deptno,"_",param_ename) INTO param_ename FROM emp WHERE ename = param_ename;
SET param_sal = param_sal * 12;
END //
delimiter;
set @param_ename = '关羽';
set @param_sal = 3000;
CALL proc08(@param_ename,@param_sal);
SELECT @param_ename;
SELECT @param_sal;
4.4 流程控制-分支语句
4.4.1 IF
IF 语句是 MySQL 存储过程中的条件分支控制 ,和 Java、Python 等编程语言的 if/else if/else 逻辑完全一致:
- 根据条件表达式的
TRUE/FALSE结果,选择执行对应的代码块; - 支持多条件判断(
IF→ELSEIF→ELSE),实现复杂分支逻辑; - 必须在
BEGIN...END代码块中使用。
语法格式:
sql
IF 条件1 THEN
-- 条件1为TRUE时执行的语句
ELSEIF 条件2 THEN
-- 条件2为TRUE时执行的语句
ELSE
-- 所有条件都为FALSE时执行的语句
END IF;
注意:
ELSEIF是连写的,中间没有空格;- 整个判断结构必须以
END IF;结尾。
举例:
sql
-- 输入员工的名字,判断工资的情况。
/*
sal < 10000:试用薪资
sal >= 10000 and sal < 20000:转正薪资
sal >= 20000:元老薪货
*/
delimiter //
CREATE PROCEDURE proc09(in param_ename VARCHAR(20))
BEGIN
DECLARE result VARCHAR(20);
DECLARE val_sal DECIMAL(7,2);
SELECT sal INTO val_sal FROM emp WHERE ename = param_ename;
IF val_sal < 10000 THEN
SET result = '试用工资';
ELSEIF val_sal >= 10000 AND sal < 20000 THEN
SET result = '转正薪资';
ELSE
SET result = '元老薪资';
END IF;
SELECT result;
END //
delimiter;
CALL proc09('关羽');
4.4.2 CASE
CASE 是 MySQL 中的等值 / 多分支判断语句 ,功能类似编程语言中的 switch-case:
- 适合固定值匹配 或多条件分支 的场景,比
IF更简洁直观; - 分为两种写法:简单 CASE(等值匹配) 和 搜索 CASE(条件匹配)。
语法格式:
sql
-- 简单case(等值匹配,类似 switch)
-- 场景:判断表达式是否等于某个固定值(如 deptno、grade)。
CASE 待匹配表达式
WHEN 值1 THEN 语句1;
WHEN 值2 THEN 语句2;
...
[ELSE 语句N;]
END CASE;
-- 搜索case(条件匹配,类似 if-elseif)
-- 场景:判断复杂条件(如 sal >= 30000、job = '经理' AND deptno = 10)。
CASE
WHEN 条件1 THEN 语句1;
WHEN 条件2 THEN 语句2;
...
[ELSE 语句N;]
END CASE;
举例:
sql
-- 流程控制语句:case
/*支付方式:
1 微信支付
2 支付宝支付
3 银行卡支付
-*/
-- 格式1
delimiter //
CREATE PROCEDURE proc10(in pay_type int)
BEGIN
CASE pay_type
WHEN 1 THEN SELECT '微信支付';
WHEN 2 THEN SELECT '支付宝支付';
WHEN 3 THEN SELECT '银行卡支付';
ELSE SELECT '其他支付';
END CASE;
END //
delimiter;
call proc10(3)
-- 格式2
delimiter //
CREATE PROCEDURE proc11(in score int)
BEGIN
CASE
WHEN score < 60 THEN SELECT '不及格';
WHEN score >=60 AND score <= 70 THEN SELECT '及格';
WHEN score >70 AND score <= 90 THEN SELECT '达标';
WHEN score > 90 AND score <=100 THEN SELECT '优秀';
END CASE;
END //
delimiter ;
call proc11(88)
4.5 流程控制-循环语句
循环是一段只写一次,但可重复执行多次的代码块,用于处理批量、重复的业务逻辑。
- 循环会执行固定次数 ,或持续执行直到特定条件满足时终止。
- 类比 Java:循环就是
for/while,让代码 "重复干活"。
循环分类(三种循环结构):
| 循环类型 | 特点 | 执行逻辑 | 类比 Java |
|---|---|---|---|
WHILE |
先判断条件,再执行语句 | 条件为 TRUE 时才执行循环体 |
while(...) { ... } |
REPEAT |
先执行语句,再判断条件 | 至少执行一次,条件为 FALSE 时继续 |
do { ... } while(...); |
LOOP |
无内置条件,需手动控制退出 | 无限循环,必须配合 LEAVE 终止 |
自定义循环(类似 while(true)) |
循环控制关键字:
| 关键字 | 作用 | 类比 Java |
|---|---|---|
LEAVE |
跳出当前循环,结束整个循环 | break |
ITERATE |
跳过本次循环剩余部分,直接进入下一次循环 | continue |
关键对比与总结:
| 维度 | WHILE |
REPEAT |
LOOP |
|---|---|---|---|
| 执行顺序 | 先判断,后执行 | 先执行,后判断 | 手动控制 |
| 最少执行次数 | 0 次 | 1 次 | 0 次(取决于 LEAVE) |
| 结束条件 | 条件为 FALSE 时结束 |
条件为 TRUE 时结束 |
需用 LEAVE 手动终止 |
| 适用场景 | 已知循环次数、条件前置 | 至少执行一次的场景 | 复杂循环逻辑、需要精确控制 |
注意:
WHILE:先看能不能做,再动手;REPEAT:先干一次,再看要不要继续;LOOP:无限循环,靠LEAVE喊停;LEAVE= 直接下班(结束循环),ITERATE= 摸鱼跳过今天(进入下一轮)。
4.5.1 while
语法格式:
sql
[标签:] WHILE 循环条件 DO
-- 循环体(要重复执行的SQL语句)
END WHILE [标签];
关键字:
- 标签 :可选,用于给循环命名,配合
LEAVE/ITERATE控制多层循环; - 循环条件 :返回布尔值(
TRUE/FALSE),为TRUE时执行循环体; DO:标记循环体开始;END WHILE:必须以分号;结尾,标记循环结束。
数据准备:
sql
-- 创建用户表(注意:user是关键字,建议加反引号 `user` 避免冲突)
CREATE TABLE `user` (
uid INT PRIMARY KEY, -- 用户ID(主键)
username VARCHAR(50), -- 用户名
password VARCHAR(50) -- 密码
);
举例:
sql
/*
【标签:】while 循环条件 do
循环体;
end while【标签】;
*/
-- 需求:向表中添加10条数据
-- 存储过程-循环-while
delimiter //
CREATE PROCEDURE proc12(in insertCount int)
BEGIN
DECLARE i int DEFAULT 1;
WHILE i <= insertCount DO
INSERT INTO user(uid,username,password) VALUES(i,CONCAT("user_",i),"123456");
SET i = i + 1;
END WHILE;
END //
delimiter ;
call proc12(10);
-- 存储过程-循环-while---leave
-- 如果i = 5 时跳出循环
TRUNCATE user;
delimiter //
CREATE PROCEDURE proc13(in insertCount int)
BEGIN
DECLARE i int DEFAULT 1;
label:WHILE i <= insertCount DO
IF i = 5 THEN
LEAVE label;
ELSE
INSERT INTO user(uid,username,password) VALUES(i,CONCAT("user_",i),"123456");
SET i = i + 1;
END IF;
END WHILE label;
END //
delimiter ;
call proc13(10);
-- 存储过程-循环-while---iterate
-- 如果i 为偶数时跳过插入(继续循环)
TRUNCATE user;
delimiter //
CREATE PROCEDURE proc14(in insertCount int)
BEGIN
DECLARE i int DEFAULT 1;
label:WHILE i <= insertCount DO
IF i % 2 = 0 THEN
SET i = i + 1;
ITERATE label;
ELSE
INSERT INTO user(uid,username,password) VALUES(i,CONCAT("user_",i),"123456");
SET i = i + 1;
END IF;
END WHILE label;
END //
delimiter ;
call proc14(10);
4.5.2 repeat
语法格式:
sql
[标签:] REPEAT
-- 循环体(要重复执行的SQL语句)
循环体语句;
UNTIL 结束条件表达式
END REPEAT [标签];
注意:
- 标签 :可选,用于给循环命名,配合
LEAVE/ITERATE控制多层循环; REPEAT:标记循环体开始;- 循环体:至少会执行一次的代码块;
UNTIL:定义结束循环的条件 ,当条件为TRUE时,循环终止;END REPEAT:必须以分号;结尾,标记循环结束。
举例:
sql
TRUNCATE user;
delimiter //
CREATE PROCEDURE proc15(in param_i int)
BEGIN
DECLARE i int DEFAULT 1;
label:REPEAT
INSERT INTO user(uid,username,password) VALUES(i,CONCAT("user_",i),"123456");
SET i = i + 1;
UNTIL i > param_i
END REPEAT label;
END //
delimiter ;
CALL proc15(10)
4.5.3 loop
语法格式:
sql
[标签:] LOOP
-- 循环体(要重复执行的SQL语句)
循环体语句;
-- 手动控制退出:满足条件时用 LEAVE 跳出循环
IF 条件表达式 THEN
LEAVE [标签];
END IF;
END LOOP;
注意:
- 标签 :强烈建议必须写 ,因为
LOOP本身没有内置结束条件,LEAVE需要通过标签指定要跳出的循环; LOOP:标记循环体开始,本身不带任何条件判断;- 循环体 :会被无限重复执行,直到
LEAVE被触发; IF + LEAVE:手动实现循环退出逻辑,是LOOP循环的核心;END LOOP:必须以分号;结尾,标记循环结束。
举例:
sql
-- 存储过程-循环控制-1oop
/*
[标签:] LOOP
-- 循环体(要重复执行的SQL语句)
循环体语句;
-- 手动控制退出:满足条件时用 LEAVE 跳出循环
IF 条件表达式 THEN
LEAVE [标签];
END IF;
END LOOP;
*/
TRUNCATE user;
delimiter //
CREATE PROCEDURE proc16(in param_i int)
BEGIN
DECLARE i int DEFAULT 1;
label:LOOP
INSERT INTO user(uid,username,password) VALUES(i,CONCAT("user_",i),"123456");
SET i = i + 1;
IF i > param_i THEN
LEAVE label;
END IF;
END LOOP label;
END //
delimiter ;
CALL proc16(20);
4.5.4 三种循环对比总结
| 维度 | WHILE |
REPEAT |
LOOP |
|---|---|---|---|
| 执行顺序 | 先判断,后执行 | 先执行,后判断 | 先执行,手动判断 |
| 最少执行次数 | 0 次 | 1 次 | 0 次(取决于 LEAVE) |
| 内置条件 | 有(WHILE 条件 DO) |
有(UNTIL 条件) |
无,需手动写 IF+LEAVE |
| 退出逻辑 | 条件为 FALSE 时退出 |
条件为 TRUE 时退出 |
触发 LEAVE 时退出 |
| 灵活性 | 中等 | 中等 | 最高(可实现任意循环逻辑) |
注意:
- 必须加标签 :
LOOP循环必须配合标签使用,否则LEAVE无法指定要跳出的循环; - 避免死循环 :循环体内必须包含改变退出条件的操作 (如
SET i = i + 1),否则永远无法触发LEAVE; - 语法规范 :
END LOOP后必须加;,LEAVE 标签;也必须以分号结尾; - 适用场景:适合复杂循环逻辑、需要精确控制退出时机的场景(如嵌套循环、动态条件退出)。
4.6 游标cursor
游标(Cursor)是一种存储查询结果集 的数据类型,专门用于在存储过程 / 函数中逐行遍历并处理查询结果。
- 作用:把多行结果集变成 "可迭代的行流",实现逐行处理(类似编程语言里的迭代器)。
- 核心操作:声明 → 打开 → 取值 → 关闭。
语法格式:
| 步骤 | 语法 | 说明 |
|---|---|---|
| 声明游标 | DECLARE 游标名 CURSOR FOR 查询语句; |
绑定一个查询结果集,不执行查询 |
| 打开游标 | OPEN 游标名; |
执行查询,将结果集加载到游标中 |
| 取值 | FETCH 游标名 INTO 变量1 [, 变量2...]; |
从结果集中读取一行数据,存入变量 |
| 关闭游标 | CLOSE 游标名; |
释放游标占用的资源,必须手动关闭 |
举例:
sql
DROP PROCEDURE IF EXISTS proc16;
delimiter //
CREATE PROCEDURE proc16(in in_dname varchar(50))
BEGIN
-- 定义局部变量
DECLARE var_empno int;
DECLARE var_ename varchar(50);
DECLARE var_sal DECIMAL(7,2);
-- 声明游标
DECLARE my_cursor CURSOR FOR SELECT empno,ename,sal from dept d,emp e where d.deptno = e.deptno and d.dname = in_dname;
-- 打开游标
OPEN my_cursor;
-- 通过游标获取值
label:loop
FETCH my_cursor into var_empno,var_ename,var_sal;
SELECT var_empno,var_ename,var_sal;
END LOOP label; -- 由于没有设置终止条件所以会报错
-- 关闭游标
CLOSE my_cursor;
END //
delimiter ;
CALL proc16('销售部')
4.7 异常处理-句柄handler
HANDLER 是 MySQL 存储过程中用于捕获和处理异常 / 条件 的机制,类似编程语言里的 try-catch,可以在发生错误时执行预设逻辑,避免程序直接崩溃。
语法格式:
sql
DECLARE handler_action HANDLER
FOR condition_value [, condition_value] ...
statement;
关键字:
| 语法部分 | 说明 |
|---|---|
DECLARE |
声明 HANDLER 的关键字,必须写在 BEGIN...END 内的最前面(变量、游标之后) |
handler_action |
捕获异常后要执行的动作: - CONTINUE:继续执行后续代码(跳过当前错误) - EXIT:终止当前 BEGIN...END 代码块(默认行为) - UNDO:回滚事务(仅支持事务性存储引擎,较少用) |
condition_value |
要捕获的异常 / 条件类型: - mysql_error_code:具体错误码(如 1062 主键冲突) - condition_name:自定义条件名- SQLWARNING:捕获所有警告类条件 - NOT FOUND:捕获 "数据未找到"(如游标 FETCH 到末尾) - SQLEXCEPTION:捕获所有严重错误类条件 |
statement |
捕获到异常后要执行的 SQL 语句(可以是 SET、SELECT 或复合语句) |
举例:
sql
DROP PROCEDURE IF EXISTS proc17;
delimiter //
CREATE PROCEDURE proc17(in in_dname varchar(50))
BEGIN
-- 定义局部变量
DECLARE flag int DEFAULT 1;
DECLARE var_empno int;
DECLARE var_ename varchar(50);
DECLARE var_sal DECIMAL(7,2);
-- 声明游标
DECLARE my_cursor CURSOR FOR SELECT empno,ename,sal from dept d,emp e where d.deptno = e.deptno and d.dname = in_dname;
DECLARE CONTINUE HANDLER FOR 1329 SET flag = 0;
-- 打开游标
OPEN my_cursor;
-- 通过游标获取值
label:loop
FETCH my_cursor into var_empno,var_ename,var_sal;
IF flag = 1 THEN
SELECT var_empno,var_ename,var_sal;
ELSE
LEAVE label;
END IF;
END LOOP label; -- 由于没有设置终止条件所以会报错
-- 关闭游标
CLOSE my_cursor;
END //
delimiter ;
CALL proc17('销售部')
5. 练习-自动创建下月每日表
需求背景:
- 业务场景:用户行为数据(搜索、购买)量极大,单表存储会导致性能瓶颈,需按天分表。
- 核心要求:每月月底提前创建下个月的每日表 ,表名格式为
user_YYYY_MM_DD(如user_2021_11_01)。 - 表结构:所有分表结构一致,用于存储当天的统计数据。
实现思路
- 参数化 :传入目标月份(如
2021-11),自动计算该月天数。 - 循环遍历:从 1 号到月末,循环生成每一天的表名。
- 动态 SQL :拼接
CREATE TABLE语句,执行创建表操作。 - 健壮性 :添加
IF NOT EXISTS避免重复创建报错,保证幂等性。
sql
-- 存储过程:创建指定月份的每日分表(修正语法错误)
DROP PROCEDURE IF EXISTS proc_create_daily_tables;
DELIMITER //
CREATE PROCEDURE proc_create_daily_tables(IN target_month VARCHAR(7)) -- 格式:'YYYY-MM'
BEGIN
DECLARE day_count INT; -- 目标月份的总天数
DECLARE current_day INT DEFAULT 1; -- 当前遍历到的日期
DECLARE table_name VARCHAR(20); -- 生成的表名
DECLARE create_sql TEXT; -- 动态建表SQL
-- 1. 计算目标月份的天数(兼容所有月份,包括2月)
SET day_count = DAY(LAST_DAY(STR_TO_DATE(CONCAT(target_month, '-01'), '%Y-%m-%d')));
-- 2. 循环创建每日表
WHILE current_day <= day_count DO
-- 拼接表名:user_YYYY_MM_DD(补零保证两位数日期)
SET table_name = CONCAT(
'user_',
REPLACE(target_month, '-', '_'),
'_',
LPAD(current_day, 2, '0')
);
-- 拼接建表SQL(关键:保证每个关键字后有空格,避免语法错误)
SET create_sql = CONCAT(
'CREATE TABLE IF NOT EXISTS ', table_name, ' (',
'id BIGINT AUTO_INCREMENT PRIMARY KEY, ',
'user_id BIGINT NOT NULL, ',
'behavior_type VARCHAR(20) NOT NULL, ', -- 行为类型:search/buy
'create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ',
'INDEX idx_user_time (user_id, create_time)',
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' -- 去掉末尾分号,避免拼接后重复
);
-- 执行动态SQL(核心修正:严格遵循PREPARE语法)
SET @sql = create_sql; -- 必须用用户变量接收,再传给PREPARE
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 日期+1,避免死循环
SET current_day = current_day + 1;
END WHILE;
-- 返回执行结果
SELECT CONCAT('成功创建 ', target_month, ' 月份的 ', day_count, ' 张每日表') AS result;
END //
DELIMITER ;
-- 调用示例:创建2021年11月的每日表
CALL proc_create_daily_tables('2026-03');
6. 存储函数
存储函数(Stored Function)是一段封装在数据库中的可复用 SQL 代码段 ,与存储过程类似,但必须返回一个值 ,可像内置函数一样在 SELECT/WHERE 等语句中直接调用。
语法格式:
sql
create function 函数名([参数名 参数类型[, ...]])
return 返回值类型
begin
-- 函数体(SQL逻辑)
return 返回值;
end;
关键字:
| 语法部分 | 说明 |
|---|---|
CREATE FUNCTION |
创建函数的关键字 |
函数名 |
自定义函数名称 |
参数名 参数类型 |
可选输入参数,格式为 参数名 数据类型,多个参数用逗号分隔 |
RETURNS 返回值类型 |
必须声明 ,指定函数返回值的数据类型(如 INT/VARCHAR/DECIMAL) |
characteristic |
可选特性,常用: - DETERMINISTIC:相同输入必返回相同结果 - NO SQL:不读写数据 - READS SQL DATA:仅读取数据 |
BEGIN...END |
包裹函数体,必须包含 RETURN 语句返回结果 |
举例:
sql
-- 允许创建函数权限信任
SET global log_bin_trust_function_creators = TRUE;
-- 创建存储函数 - 没有参数
DROP FUNCTION IF EXISTS myfunc1_emp;
delimiter //
create function myfunc1_emp()
RETURNS INT
BEGIN
DECLARE cnt int DEFAULT 0;
SELECT count(*) into cnt from emp;
RETURN cnt;
END //
delimiter ;
SELECT myfunc1_emp();
-- 创建存储函数 - 有参数
-- 传入员工编号,返回员工姓名
DROP FUNCTION IF EXISTS myfunc2_emp;
delimiter //
create function myfunc2_emp(in_empno int)
RETURNS VARCHAR(20)
BEGIN
DECLARE out_name varchar(20);
SELECT ename into out_name FROM emp WHERE empno = in_empno;
RETURN out_name;
END //
delimiter ;
SELECT myfunc2_emp(1001);
7. 存储函数 vs 存储过程
核心区别:
| 维度 | 存储函数 | 存储过程 |
|---|---|---|
| 返回值 | 必须返回一个值 | 可以无返回值,或通过 OUT/INOUT 参数返回 |
| 调用方式 | 可在 SELECT/WHERE 等语句中直接调用 |
必须用 CALL 语句调用 |
| 事务支持 | 一般不允许开启事务 | 支持事务操作 |
| 适用场景 | 纯计算、数据查询、返回单一结果 | 复杂业务逻辑、批量操作、多结果输出 |
注意:
- 必须
RETURN:函数体必须包含RETURN语句,否则语法报错; - 参数与返回值类型:参数和返回值都必须显式指定数据类型;
- 权限与特性 :创建函数需
CREATE ROUTINE权限,建议添加DETERMINISTIC/READS SQL DATA等特性以兼容 MySQL 8.0+; - 避免副作用 :函数应尽量为纯函数(相同输入返回相同结果),不要在函数中做写操作(如
INSERT/UPDATE)。