介绍

MySQL从5.0版本开始支持存储过程和函数。存储过程和函数能够将复杂的 SQL 逻辑封装在一起,应用程序无须关注存储过程和函数内部复杂的 SQL 逻辑,而只需要简单地调用存储过程和函数即可。

一句话总结:存储过程是一组为了完成特定功能的 SQL 语句集合。

关键字:封装重用

基本语法

基本语法包括存储过程的创建、调用、查看、与删除。

创建存储过程:

CREATE PROCEDURE 存储过程名称 ([ 参数列表 ])
BEGIN
    -- SQL语句
END ;

调用存储过程:

CALL 名称 ([ 参数 ]);

查看存储过程:

1. 查询某个存储过程的定义
SHOW CREATE PROCEDURE 存储过程名称;

2. 查询指定数据库的存储过程及状态信息
SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx';

删除存储过程:

DROP PROCEDURE [ IF EXISTS ] 存储过程名称;

简单的例子

前面提到了,存储过程是为了完成特定任务的 SQL 集合,可以只有一条 SQL 语句,也可以有多条。即存储过程可以非常简单也可以非常复杂,下面就通过一个简单的例子来说明存储过程的基本使用。

创建一个查询用户表所有数据的存储过程:

CREATE PROCEDURE `p_user`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT '查询用户表的所有数据'
BEGIN
    SELECT * FROM tb_user;
END

调用该存储过程:

CALL p_user();

结果:
p_user.png

查看该存储过程:

SHOW CREATE PROCEDURE p_user;

p_user_show.png

删除该存储过程

DROP PROCEDURE IF EXISTS p_user;

通过这个简单的例子,我们使用了一遍存储过程的基本语法,但是想要写出更为复杂的存储过程,还需要了解“变量”、“分支”、“循环”、“参数”、“游标”等概念。

更复杂的存储过程

除了“游标”,“变量”、“分支”、“循环”、“参数”等概念与其他后端语言的类似。

变量

MySQL 中,变量分为系统变量、用户定义变量和局部变量。因为存储过程中更多的是用局部变量,所以这里只记录局部变量的使用。

变量的声明、赋值与输出:

1. 声明
DECLARE 变量名 变量类型 [DEFAULT ... ] ;

2. 赋值
SET 变量名 := 值 ;    
SELECT 字段名 INTO 变量名 FROM 表名 ... ; # 将表中某个字段的值赋值给变量

3. 输出
SELECT 变量名;

例子,声明一个 name 变量,并将其赋值为申玉非:

CREATE PROCEDURE `p_var`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
    DECLARE name VARCHAR(50);
    SET name := "申玉非";
    SELECT name;
END

判断

MySQL 中,用于判断的语句有两种:ifcase

if 判断:

IF 条件1 THEN
    .....
ELSEIF 条件2 THEN       -- 可选
    .....
ELSE                    -- 可选
    .....
END IF;

例子,根据分数判断分数段,例如 71 分就是良好:

CREATE DEFINER=`root`@`localhost` PROCEDURE `p_if`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT '测试 if 语句'
BEGIN
    DECLARE score INT DEFAULT 89;   # 声明 score 变量并赋值为 89
    DECLARE result VARCHAR(50);     # 声明 result 变量,注意这里的数据类型要定义长度。

    if score >= 0 && score < 60 then
        SET result := '不及格';
    ELSEIF score >= 60 && score < 80 then
        SET result := '良好';
    ELSEIF score >= 80 && score < 100 then
        SET result := '优秀';
    ELSEIF score = 100 then
        SET result := '满分';
    ELSEIF score < 0 || score > 100 then
        SET result := '分数不合法';
    end if;

    SELECT result;
END

case 判断:

CASE case_value
    WHEN when_value1 THEN statement_list1
    [ WHEN when_value2 THEN statement_list2] ...
    [ ELSE statement_list ]
END CASE;

含义:

  • case_value 的值为 when_value1 时,执行 statement_list1;
  • 当值为 when_value2 时,执行 statement_list2;
  • 否则就执行 statement_list。

例子,根据分数判断分数段:

CREATE DEFINER=`root`@`localhost` PROCEDURE `p_case`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
    DECLARE score INT DEFAULT 89;
    DECLARE result VARCHAR(50);
    DECLARE my_score INT(11);
    SET my_score := FLOOR(score / 10);

    case 
    when my_score >= 0 && my_score < 6 then
        SET result := '不及格';
    when my_score = 6 then
        SET result := '及格';
    when my_score = 7 then
        SET result := '良好';
    when my_score = 8 then
        SET result := '优秀';
    when my_score = 9 then
        SET result := '非常优秀';
    when my_score = 10 then
        SET result := '满分';
    when my_score < 0 || my_score > 10 then
        SET result := '分数不合法,分数必须在 0~100 之间';
    END case;

    SELECT result;
END

参数

MySQL 中参数分为 3 类:

类型描述
IN该类参数作为输入,也就是需要调用时传入值
OUT该类参数作为输出,也就是该参数可以作为返回值
INOUT既可以作为输入参数,也可以作为输出参数

例子,传入两个数,返回两个数之和:

CREATE PROCEDURE `p_param`(
    IN `x` INT,     # 第一个传入的参数
    IN `y` INT,     # 第二个传入的参数
    OUT `s` INT     # 返回值
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT '测试参数'
BEGIN
    SET s := x + y;
END

调用:

-- 定义用户变量 @result来接收返回的数据, 用户变量可以不用声明

CALL p_param(1, 2, @s);
SELECT @s AS s;

# 结果为 3;

循环

MySQL 中有三种循环:whilerepeatloop

while

while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的 SQL 语句,语法如下:

-- 先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DO
    SQL 逻辑...
END WHILE;

例子,从 1 加到 10:

CREATE PROCEDURE `p_sum`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT '从 1 加到 10'
BEGIN
    DECLARE n INT DEFAULT 1;
    DECLARE sum INT DEFAULT 0;
    while n <= 10 DO
        SET sum := sum + n;
        SET n := n + 1;
    END while;

    SELECT sum;
END

# 调用
CALL p_sum(); 
# 结果:55

repeat

repeat 是有条件的循环控制语句, 当满足 until 声明的条件的时候,则退出循环,语法如下:

-- 先执行一次逻辑,然后判定UNTIL条件是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEAT
    SQL 逻辑...
    UNTIL 条件
END REPEAT;

do...while 类似,但又不同:

  • do...while满足条件则循环,否则退出;
  • repeat不满足条件则循环,否则就退出。

例子,从 1 加到 10:

CREATE PROCEDURE `p_sum`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT '从 1 加到 10'
BEGIN
    DECLARE n INT DEFAULT 1;
    DECLARE sum INT DEFAULT 0;

    repeat
        SET sum := sum + n;
        SET n := n + 1;
        until n > 10
    END repeat;

    SELECT sum;
END

# 调用
CALL p_sum();
# 结果:55

注意: until语句后面没有“;”。

loop

loop 相对简单,是没有条件的循环。但是配合 label 却可以实现“终止循环” 和 “跳过循环”,语法如下:

[begin_label:] LOOP
    SQL 逻辑...
END LOOP [end_label];
  • 终止循环:LEAVE label;
  • 跳过循环:ITERATE label;

例子:求 10 以内(包括10)的偶数和:

CREATE PROCEDURE `p_sum`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT '求 10 以内(包括 10)的偶数和'
BEGIN
    DECLARE n INT DEFAULT 1;
    DECLARE sum INT DEFAULT 0;

    s:loop                # s 是标签名
        if n > 10 then
            leave s;      # 终止循环
        END if;
    
        if n % 2 = 0 then
            SET sum := sum + n;
        END if;
        SET n := n + 1;
    END loop s;
    
    SELECT sum;
END

# 调用
p_sum();
# 结果:30

游标

前面提到,我们可以把数据表中某个字段的值赋值给变量:

SELECT 字段名 INTO 变量名 FROM 表名 ... ;

现在我们想把查询到的结果存到另一张表中,光靠变量是不够的,因为一个变量只能存储一个数据。我们就会想要是在 MySQL 中有数组就好了,我们可以把查出来的数据保存到数组中,然后循环添加数据。很可惜,在 MySQL 中没有数组,但是有一个类似的概念——游标。

游标(CURSOR)是用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理。

和把大象塞进冰箱一样简单,使用游标分为四步:

  • 声明游标;
  • 打开游标;
  • 读取游标;
  • 关闭游标。

声明游标:

DECLARE 游标名称 CURSOR FOR 查询语句;

打开游标:

OPEN 游标名称;

读取游标:

FETCH 游标名称 INTO 变量 [, 变量 ];

关闭游标:

CLOSE 游标名称;

要实现把查询出来的结果保存到另外的表,光有游标还不够。我们遍历数组的时候,什么时候停止遍历?遍历到数组的最后一个元素的时候。在大多数编程语言中,都有手段来统计数组的个数,把数组的个数作为终止循环的判断条件。但是在 MySQL 中,并没有这样的手段,也就意味着没有终止循环的条件,一旦越界,就会报错。

那这个问题是否是无解的呢?答案是否定的,在 MySQL 中,恰恰就是利用这个报错来终止循环,也就是利用 MySQL 中的“条件处理程序”来打上这个补丁。

条件处理程序(Handler)可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。语法为如下:

DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement ;

handler_action 的取值:
    CONTINUE: 继续执行当前程序
    EXIT: 终止执行当前程序

condition_value 的取值:
    SQLSTATE sqlstate_value: 状态码,如 02000
    SQLWARNING: 所有以01开头的SQLSTATE代码的简写
    NOT FOUND: 所有以02开头的SQLSTATE代码的简写
    SQLEXCEPTION: 所有没有被SQLWARNING 或 NOTFOUND捕获的SQLSTATE代码的简写

游标和条件处理程序都介绍完了,下面举一个把性别为女的用户保存到 tb_user_women 表中的例子:

CREATE DEFINER=`root`@`localhost` PROCEDURE `p_cursor`(
    IN `ugender` INT
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
    DECLARE uname VARCHAR(50);
    DECLARE uage INT(11);
    DECLARE u_cursor CURSOR FOR SELECT name,age FROM tb_user WHERE gender = ugender; 
    -- 声明条件处理程序 : 当SQL语句执行抛出的状态码为02000时,将关闭游标u_cursor,并退出
    DECLARE exit handler FOR SQLSTATE   '02000' close u_cursor;

    -- 创建 tb_user_women 表
    DROP TABLE if EXISTS tb_user_women;
    CREATE TABLE if not exists tb_user_women(
        id INT(11) PRIMARY KEY  AUTO_INCREMENT,
        name VARCHAR(50),
        age INT(11)
    ); 

    -- 打开游标
    OPEN u_cursor;

    -- 读取游标
    while TRUE DO
        fetch u_cursor INTO uname, uage;
        INSERT INTO tb_user_women(name,age) VALUES(uname,uage);
    END while;
    -- 关闭游标
    close u_cursor;
END

# 调用
CALL p_cursor(0);

结果:
p_cursor.png

注意:声明自定义变量要写在声明游标前面。

至于 p_cursor 存储过程中,为什么要把条件处理程序中的 SQLSTATE 的值设置为 02000,可以参考 MySQL 官方文档——错误状态码

优缺点

优点

  • 存储过程可以一次编译多次使用;
  • 存储过程的安全性强;
  • 可以减少网络传输量。

缺点

  • 可移植性差。存储过程不能跨数据库移植,比如在 MySQL、Oracle 和 SQL Server 里编写的存储过程,在换成其他数据库时都需要重新编写。
  • 存储过程的版本管理很困难。比如数据表索引发生变化了,可能会导致存储过程失效。我们在开发软件的时候往往需要进行版本管理,但是存储过程本身没有版本控制,版本迭代更新的时候很麻烦。
  • 它不适合高并发的场景。高并发的场景需要减少数据库的压力,有时数据库会采用分库分表的方式,而且对可扩展性要求很高,在这种情况下,存储过程会变得难以维护,增加数据库的压力,显然就不适用了。

参考

  1. MySQL 视图/存储过程/触发器
  2. 存储过程与存储函数
  3. 官方文档——错误状态码

Moonshadow2333
28 声望0 粉丝

征途漫漫