MySQL存储过程

一.存储过程

1.什么是存储过程

存储过程是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字和参数来执行,并获取相应的结果。

存储过程类比于C++中的函数。在项目中,当用户进行某种操作后,服务器端就要对用户的数据进行更新(比如游戏中的天梯分数,玩家赢了一局,此时我们就要调用对应的接口,对玩家的分数进行更新,并且写入到数据库中),这一业务逻辑是直接在应用层实现的。而存储过程则是将这一步移到了数据库中,即更新游戏天梯分数的这一步直接交由数据库层完成。

1.1.存储过程的特点

  • 封装性:将业务逻辑封装在数据库内部,减少应用程序的复杂性;
  • 可维护性:集中管理数据库操作,便于维护和更新,表结构更新了,不必修改应用层,做到一处修改,处处生效;
  • 可重用性:像函数一样,编译好后可以被多次调用,提高代码的重用性。

1.2.存储过程的优缺点

优点:

  • 性能优化:存储过程在创建好后编译并存储在数据库中,执行速度比单个SQL快;

  • 代码重用:存储过程可以重复调用,减少重复代码,提高代码的可维护性;

  • 安全性:可以限制应用层直接访问数据库,通过存储过程间接访问,从而保证数据库的安全性;

  • 事务管理:可以在存储过程中实现复杂的事务逻辑;

  • 降低耦合:当表结构发生变化时,只需要修改相应的存储过程,应用程序的改动小
    缺点:

  • 可移植性差:存储过程不能跨数据库移植,更换数据库时需要重新编写;

  • 调试困难:少数数据库支持存储过程的调试;

  • 不适合高并发场景:在高并发场景下,同时有上万条SQL语句要数据库执行,如果此时再让数据库自己实现业务逻辑(存储过程)无异于雪上加霜。

2.创建存储过程的语法

sql 复制代码
DELIMITER //                    -- 修改SQL语句的结束符

-- 创建存储过程
CREATE PROCEDURE name(参数列表)
BEIGN
    ... -- SQL语句
END//

DELIMITER ;                     -- 重设SQL语句的结束符

-- 调用存储过程
CALL name();

-- 查看指定数据库中的存储过程
SELECT * FROM information.schema.ROUTINES WHERE ROUTINE_SCHEMA = '数据库名';

-- 查看定义存储过程的具体细节
SHOW CREATE PROCEDURE 存储过程名;

-- 删除存储过程
DROP PROCEDURE 存储过程名;

DELIMITER // :

修改SQL语句的结束符,在图形化界面上写SQL时,会根据缩进来确认一个SQL的结束。但是在命令行,默认是以";"作为一个SQL的结束,如果在begin和end之间,SQL语句出现了";",就认为结束了。会导致我们的存储过程被解析失败。

delimiter // 的作用就是将SQL语句的结束符修改为另一个不容易冲突的符号,这样在解析SQL语句时就会以//作为结束符,而不会遇到分号提前结束

2.1利用存储过程,查看所有学生的姓名以及对应的总分

sql 复制代码
DELIMITER //

CREATE PROCEDURE p_sum()
BEGIN
  SELECT name, chinese + math + english AS total FROM exam;
END//

DELIMITER ;


CALL p_sum(); -- 调用存储过程

二.变量

MySQL中变量分为三类:系统变量,用户自定义变量,局部变量。

1.系统变量

系统变量是MySQL服务器的配置变量,控制着服务器的行为和性能。

分为全局系统变量和会话系统变量,全局系统变量由配置文件来,会话系统变量由全局来。

查看系统环境变量

sql 复制代码
-- 查看所有系统变量
show global variables;
show session variables;

-- 进行模糊匹配,%用作通配符
show global variables like 'auto%';
show session variables like 'auto%';

-- select @@global.具体系统变量名; @@表示系统变量
select @@session.autocommit;

设置系统环境变量

sql 复制代码
-- 设置系统环境变量
set session autocommit = 0;
set @@global.autocommit = 0;
  • 如果没有指定session/global,默认修改和查询都是session系统变量;
  • 不论是对会话还是全局的系统变量进行修改,都只是内存级的,MySQL重启后就会重置。想要永久修改系统变量,则需要修改配置文件。

2.用户自定义变量

  • 用户自定义变量,用一个@表示
  • 用户自定义变量在当前会话有效,通常用来定义一些查询的中间结果,或者表示一些条件,方面后面的查询使用
  • 在定义用户自定义变量时,既可以使用 = ,也可以使用:=,但=又可以用来作为判断等于,所以为了避免歧义,推荐使用:=
sql 复制代码
set @age = 18;

set @age := 99;

select @age := 100;

select count(*) into @stu_count from student;

如果在select后面直接使用 = 则表示比较,不是赋值。

3.局部变量

局部变量,只在存储过程、函数、触发器中有效。使用时需要使用declare声明,作用域在begin end之间。

declare name type [default 值] -- 声明局部变量

set name = 值; -- 局部变量赋值

set name := 值; -- 赋值

select 列名 into name from 表名; -- 赋值,将查询结果赋值给局部变量

sql 复制代码
delimiter //

-- 创建存储过程
create procedure p_count()
BEGIN
  -- 实现方法,定义局部变量
  DECLARE stu_count int default 0;
  
  -- 给局部变量赋值
  select count(*) into stu_count from student;
  
  select stu_count;
END//

delimiter;

-- 调用存储过程
call p_count();

4.注意事项

  • 变量名不区分大小写;
  • 在存储过程和函数中,局部变量必须在使用前声明;
  • 用户自定义变量在会话结束时失效,而局部变量在存储过程/函数结束时失效;

三.SQL编程

结构化查询语⾔(Structured Query Language)简称SQL,是⼀种特殊的编程语⾔,是⼀ 种数据库查询和程序设计语⾔,⽤于存取数据以及查询、更新和管理关系数据库系统。

1.if语句

使用if语句,根据分数,判断等级:

sql 复制代码
-- if语句
delimiter //

create procedure p1()
BEGIN
  declare score int DEFAULT 86;
  declare result varchar(10);
  
  if score >= 90 THEN
    set result := '优秀';
  elseif score >= 80 and score < 90 THEN
    set result := '良好';
  elseif score >= 60 and score  < 80 THEN
    set result := '及格';
  else 
    set result := '不及格';
  end if;
  
  select result;

END//

delimiter;

call p1();

2.SQL参数

-- 参数分为 IN(输入) OUT(输出) INOUT(输入输出)

-- 对于输入型参数而言,只做为参数传递

-- 对于输出型参数而言,没有初始值,只用作接收结果

-- 对于输入输出型参数而言,既可以传递值,也可以接收值

sql 复制代码
delimiter //

create procedure p2(in score int, out result varchar(10))
BEGIN
  if score >= 90 THEN
    set result := '优秀';
  elseif score >= 75 and score < 90 THEN
    set result := '良好';
  elseif score >= 60 and score < 75 THEN
    set result := '及格';
  ELSE
    set result := '不及格';
  end if;

end//

delimiter;

call p2(90, @result);
select @result;


-- 输入输出型参数
delimiter //

create procedure p3(inout score int)
BEGIN
  set score := score + 10;
end//

delimiter;

set @score := 999;
call p3(@score);
select @score;

3.case语句

在SQL中,case语句有两种写法,第一种即C++中的switch case语句;第二种即是对if else的一种变形

每一个选项下的语句可以是一句也可以是多句但不允许为空。可以使用begin end。

sql 复制代码
delimiter //
create procedure p4(IN code INT, OUT result VARCHAR(50))
BEGIN
  CASE code
	WHEN 0 THEN
		set result := 'ok';
  WHEN 1 THEN
    set result := 'warning';
  WHEN 2 THEN
    set result := 'error';
  WHEN 3 THEN
    set result := 'fatal';
	ELSE
    set result := 'unknow';
  END CASE;

END//
delimiter ;


call p4(-1, @result);
select @result;


delimiter //
create procedure p5(IN month int, OUT result VARCHAR(50))
BEGIN
  case
    when month >= 3 and month <= 5 THEN
      set result := '春季';
    when month >= 6 and month <= 8 THEN
      set result := '夏季';
    when month >= 9 and month <= 11 THEN
      set result := '秋季';
    when month = 12 or month = 1 or month = 2 THEN
      set result := '冬季';
    ELSE
      set result := 'unknow';
  end case;
END//
delimiter ;

call p5(97, @result);
select @result;

4.while循环

类似于C++中的while循环,循环条件:是否继续的条件,满足则进入循环。

sql 复制代码
delimiter //
CREATE PROCEDURE p6(IN n INT)
BEGIN
  -- 定义结果和
  DECLARE sum INT DEFAULT 0;
  
  -- 循环
  WHILE n > 0 DO
    SET sum := sum + n;
    SET n := n - 1;
  END WHILE;
  
  SELECT sum;
END//
delimiter ;

5.repeat循环

类似于do while循环,判断条件为true,退出循环,即repeat的判断条件是退出条件

sql 复制代码
delimiter //
CREATE PROCEDURE p7(IN n INT)
BEGIN
  -- 定义结果和
  DECLARE sum INT DEFAULT 0;
  
  -- repeat循环
  REPEAT 
    SET sum := sum + n;
    SET n := n - 1;
  UNTIL n <= 0 END REPEAT;
  
  SELECT sum;
END//

delimiter ;

6.loop循环

本质上是一个死循环,需要通过循环体内部的if条件控制循环

leave 跳出循环,iterate 跳出本次循环

使用loop循环时,要对loop循环起一个名字,用来后序指定跳出循环

sql 复制代码
-- 计算1 ~ n的和

delimiter //
CREATE PROCEDURE p8(IN n INT)
BEGIN
  DECLARE sum INT DEFAULT 0;

  sum_lable : LOOP
    -- 循环退出条件
    IF n <= 0 THEN
      LEAVE sum_lable;
    END IF;
    
    SET sum := sum + n;
    SET n := n - 1;
  END LOOP sum_lable;
  
  SELECT sum;
END//

delimiter ;

-- 使用loop循环,跳过某次循环:只计算1~n的偶数和

delimiter //
CREATE PROCEDURE p9(IN n INT)
BEGIN
  DECLARE sum INT DEFAULT 0;
  
  sum_lable : LOOP
    IF n <= 0 THEN 
      LEAVE sum_lable;
    END IF;
    
    IF n % 2 = 1 THEN
      SET n := n - 1;
      ITERATE sum_lable;
    END IF;
    
    SET sum := sum + n;
    SET n := n - 1;
  END LOOP sum_lable;
  
  SELECT sum;
END//
delimiter ;

四.游标

游标是MySQL数据库中的一个数据库对象,用来在存储过程/函数中对查询到的结果集进行逐行检索。

游标四板斧:

  • DECLARE 游标名 cursor for 查询语句; -- 声明游标
  • open 游标名; -- 打开游标
  • fetch 游标名 into 变量 -- 使用游标检索结果集 ,将检索到的结果写入到变量中,该变量必须提前声明
  • close 游标名; -- 关闭游标

使用下述存储过程时,会遇到一个问题,我们通过游标遍历结果集时,怎么退出循环呢?当然时遍历到越界就结束,可是怎么判断是否越界呢?游标并没有规定。

如下代码,一致检索,当检索到结束位置时,此时继续检索,就会出错:表示没有数据

要解决这个问题,我们需要结合条件处理程序来解决!!!

sql 复制代码
delimiter //
CREATE PROCEDURE p10(IN class_id INT)
BEGIN
  -- 新表字段,用来存储游标检索到的变量
  DECLARE student_name VARCHAR(32);
  DECLARE class_name VARCHAR(32);

  -- 创建游标
  DECLARE stu_cursor CURSOR FOR 
    SELECT s.name student_name, c.name class_name 
    FROM class c inner join student s on c.id = s.class_id where c.id = class_id;
    
  -- 创建新表
  DROP TABLE IF EXISTS t_student_class;
  CREATE TABLE t_student_class(
    id BIGINT PRIMARY KEY auto_increment,
    student_name VARCHAR(32) NOT NULL,
    class_name VARCHAR(32) NOT NULL
  );
  
  -- 打开游标
  OPEN stu_cursor;
  
  -- 使用游标检索结果集
  WHILE TRUE DO
    FETCH stu_cursor INTO student_name, class_name; -- TODO:如何结束循环,fetch最后肯定会遍历到空
    
    -- 将检索结果插入到新表中
    INSERT INTO t_student_class (student_name, class_name) values (student_name, class_name);
  END WHILE;
  
  -- 关闭游标
  CLOSE stu_cursor;
END//

delimiter ;

五.条件处理程序

条件处理程序就类似于C++中的异常机制,使用条件处理程序 保证存储过程/函数在遇到警告或者错误时能继续执行,可以增强程序处理问题的能力,避免程序异常停止运行。

  • 定义条件,事先定义好可能出现的问题
  • 处理程序,定义遇到问题所采取的处理方法

语法

sql 复制代码
-- 语法
-- 声明条件处理程序
DECLARE handler_action HANDLER
  FOR condition_value[多个]
  statement

-- handler_action就是遇到问题后,应该怎么处理,是直接退出,还是继续向下运行
handler_action:{
  CONTINUE
  | EXIT
}

-- condition_value 即可能遇到的问题所对应的错误码,有mysql级别的,也有sql级别的
condition_value {
  mysql_error_code                     -- MySQL错误码
  SQLSTATE[value]sqlstate_value        -- 状态码
  SQLWARNING                           -- 所有01开头的SQLSTATE的状态码
  NOT FOUND                            -- 所有02开头的SQLSTATE状态码
  SQLEXCEPTION                         -- 其他状态码
}

使用条件处理程序,解决使用游标导致访问空行

sql 复制代码
DROP PROCEDURE IF EXISTS p11;
delimiter //
CREATE PROCEDURE p11(IN class_id INT)
BEGIN
  -- 定义变量
  DECLARE student_name VARCHAR(32);
  DECLARE class_name VARCHAR(32);
  DECLARE is_done bool DEFAULT FALSE;
  
  -- 声明游标
  DECLARE stu_cursor CURSOR FOR 
    SELECT s.name student_name, c.name class_name 
    FROM class c inner join student s on c.id = s.class_id where c.id = class_id;
    
  -- 声明条件处理程序
  DECLARE CONTINUE HANDLER
    FOR NOT FOUND
    SET is_done := TRUE;
    
  -- 创建新表
  DROP TABLE IF EXISTS t_student_class;
  CREATE TABLE t_student_class(
    id BIGINT PRIMARY KEY auto_increment,
    student_name VARCHAR(32) NOT NULL,
    class_name VARCHAR(32) NOT NULL
  );
  
  -- 打开游标
  OPEN stu_cursor;
  
  -- 使用游标进行检索结果集
  -- 如果使用while循环进行检索,当fetch遍历到空出错时,虽然is_done已经被设置为ture,但处理程序为继续向下运行
  -- 此时会将游标重置为上一次使用的位置,并且继续执行之后的代码,导致重复插入最后一行
  -- 所以,我们这里使用loop循环,手动控制循环退出条件
--   WHILE NOT is_done DO 
--     FETCH stu_cursor INTO student_name, class_name;
--     
--     INSERT INTO t_student_class (student_name, class_name) VALUES (student_name, class_name);
--   END WHILE;

  stu_loop: LOOP
    FETCH stu_cursor INTO student_name, class_name;

    IF is_done THEN
      LEAVE stu_loop; 
    END IF; 
    
    INSERT INTO t_student_class (student_name, class_name) VALUES (student_name, class_name);
  
  END LOOP stu_loop;

  
  -- 关闭游标
  CLOSE stu_cursor;
END//

delimiter ;

注意:

条件处理程序必须声明在游标之后,局部变量要声明在存储过程/函数的最前面。

六.存储函数

存储函数,带有返回值的存储过程,并且要求参数只能是IN类型,在指定参数时,不需要指定IN类型,否则会报错。

存储函数与存储过程唯一的区别就是,存储函数一定有返回值,存储过程可以有可以没有(通过输出型参数)。

1.语法

sql 复制代码
-- 定义存储函数的语法
CREATE FUNCTION name(参数列表)
RETURNS type [characteristic...]
BEGIN
  ...
  RETURN ...;
END;

-- 在MySQL8.0,默认开启了binlog,如果开了binlog,定义存储函数时,需要指定characteristic
-- 指定characteristic并不会对存储函数进行修改,是在MySQL底层起某种作用
characteristic:
  [NOT] DETERMINISTIC -- 相同的输入,结果总是一致
  NO SQL              -- 不包含SQL语句
  READS SQL           -- 包含读取数据的SQL语句
  MODIFIES SQL        -- 包含写入数据的SQL语句

2.使用存储函数,实现1~n的累加

sql 复制代码
-- 定义存储函数,使用1~n的累加
delimiter //
CREATE FUNCTION func1(n INT)
RETURNS INT DETERMINISTIC
BEGIN
  DECLARE sum INT default 0;
  
  WHILE n > 0 DO
    SET sum := sum + n;
    SET n := n - 1;
  END WHILE;
  
  RETURN sum;

END//
delimiter;
  
-- 调用存储函数与调用MySQL的内置函数一致,使用select + 存储函数名()
SELECT func1(10);
  

七.触发器

  • 触发器是一个与表关联的数据库对象,在对表进行insert、update、delete时,触发并执行定义触发器时的SQL语句。
  • 触发器可以在对表的操作之前/之后执行,称为触发时间,before,after
  • 触发器可以执行SQL语句或其他逻辑块,用于实现复杂的业务逻辑/数据验证
  • MySQL支持3中触发器类型,insert、update、delete触发器。在触发器中使用new/old关键字访问操作后的数据
  • 行级触发器,修改了多少行,就调用多少次
  • 语句级触发器,调用了多少次修改语句,执行多少次触发器
  • MySQL只支持行级触发器
sql 复制代码
-- 定义触发器的语法
CREATE TRIGGER trg_name
  trigger_time, trigger_event -- 触发器的触发时间,以及因为什么事件导致触发器触发
  ON table_name, FOR EACH ROW -- 触发器关联表明,行级触发器
BEGIN
  trigger_stmt;
END;

-- 查看当前表中所有的触发器
SHOW TRIGGERS;

-- 查看定义触发器时的细节
SHOW CREATE TRIGGER trg_name;

-- 删除触发器
DROP TRIGGER IF EXISTS trg_name;

通过触发器记录学⽣表的变更⽇志,将变更⽇志写⼊⽇志表student_log中,包含增加,修改和删 除操作。

sql 复制代码
-- insert 触发器
delimiter //
CREATE TRIGGER IF NOT EXISTS trg_student_insert
  AFTER INSERT
  ON student FOR EACH ROW
BEGIN
  -- 插入一条新记录时,将该修改记录到student_log中
  INSERT INTO student_log (
    operation_type,
    operation_time,
    operation_id,
    operation_data
    )
  VALUES (
    'insert',
    now(),
    new.id,
    CONCAT(new.id, '-', new.name, '-', new.sno, '-', new.age, '-', new.gender, '-', new.enroll_date, '-', new.class_id)
    );
END//
delimiter ;
相关推荐
qq_417908443 小时前
com.mysql.cj.jdbc.Driver 解析
数据库·mysql
我要升天!4 小时前
MySQL表的内连和外连
android·mysql·adb
jingfeng5145 小时前
MySQL库的操作(ubuntu)
数据库·mysql
叱咤少帅(少帅)12 小时前
DML语句
mysql
编码追梦人14 小时前
探索 Docker/K8s 部署 MySQL 的创新实践与优化技巧
mysql·docker·kubernetes
2351615 小时前
【MySQL】数据库事务深度解析:从四大特性到隔离级别的实现逻辑
java·数据库·后端·mysql·java-ee
conkl15 小时前
Flask 与 MySQL 数据库集成:完整的 RESTful API 实现指南
数据库·mysql·flask
似水流年,是谁苍白了等待18 小时前
Spring Boot + MyBatis plus + MySQL 实现位置直线距离实时计算
spring boot·mysql·mybatis
小园子的小菜19 小时前
MySQL 查询与更新语句执行过程深度解析:从原理到实践
数据库·mysql