9、MYSQL-存储过程

存储过程

1. 什么是存储过程

  • 版本支持:MySQL 5.0 及以上版本开始支持。
  • 本质定义 :一组预先编译的 SQL 语句集合,是数据库层面的代码封装与复用,功能类似 Java 中的方法 / Python 中的函数。
  • 核心作用:可以实现复杂的业务逻辑,将多条 SQL 或流程控制封装为一个可调用的单元。

2. 核心特性

特性 详细说明
参数与变量 支持输入(IN)、输出(OUT)、输入输出(INOUT)参数,可自定义变量存储中间结果。
流程控制 支持 IF/ELSECASEWHILELOOP 等控制语句,能实现分支、循环等复杂逻辑。
模块化复用 像函数一样封装业务逻辑,一次定义、多次调用,提升代码可维护性。
执行高效 首次执行时完成编译与优化,后续调用直接执行,避免重复解析 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_counttotal_sal),避免与表字段重名。
数据类型 与 MySQL 字段类型一致,如 INTVARCHAR(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)来永久改变默认值。
  1. 会话变量
  • 每次新建连接时,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输入输出参数 ,兼具 INOUT 的特性:

  • 既进又出:先从外部传入初始值,在存储过程内部修改后,再将新值返回给外部调用者;
  • 传入的变量会被直接修改,过程执行结束后,外部变量的值会同步更新为过程内修改后的值;
  • 类比 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 结果,选择执行对应的代码块;
  • 支持多条件判断(IFELSEIFELSE),实现复杂分支逻辑;
  • 必须在 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 语句(可以是 SETSELECT 或复合语句)

举例:

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)。
相关推荐
Arya_aa2 小时前
Mysql数据库-管理和存储数据库(开源管理系统)与JDBC操作数据库步骤,JUnit以及如何将压缩包中exe程序添加上桌面图标
数据库·mysql·junit·开源
最懒的菜鸟3 小时前
redis缓存击穿
数据库·redis·缓存
qq_404265833 小时前
用Python批量处理Excel和CSV文件
jvm·数据库·python
人间打气筒(Ada)4 小时前
mysql数据库之DDL、DML
运维·数据库·sql·mysql·dba·dml·dql
KingCruel4 小时前
MySQL JSON 数据操作
mysql·json
代码派4 小时前
信创迁移“不敢切”的最后一公里:数据一致性校验怎么做才算够?
数据库·数据库开发·dba·etl工程师·数据库管理工具·信创数据库·信创迁移
qq_418101774 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
熙胤4 小时前
PostgreSQL 向量扩展插件pgvector安装和使用
数据库·postgresql
牢七5 小时前
baijiacms-master 审计
数据库