MySQL进阶-存储过程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • [1. 存储过程](#1. 存储过程)
    • [1.1 初识](#1.1 初识)
    • [1.2 环境准备](#1.2 环境准备)
    • [1.3 语法](#1.3 语法)
    • [1.4 系统变量](#1.4 系统变量)
    • [1.5 SQL编程](#1.5 SQL编程)
    • [1.6 游标](#1.6 游标)
    • [1.7 条件处理程序](#1.7 条件处理程序)
    • [1.8 存储函数](#1.8 存储函数)
    • [1.9 触发器](#1.9 触发器)
    • [1.10 常见面试题](#1.10 常见面试题)
  • 总结

前言

1. 存储过程

1.1 初识

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

其实存储过程就是一个用SQL语言实现的方法

特点

• 封装性:将业务逻辑封装在数据库内部,减少应⽤程序的复杂性。

• 可维护性:集中管理数据库操作,便于维护和更新。

• 可重⽤性:可以被多次调⽤,提⾼代码的重⽤性。

优点

• 性能优化:存储过程在创建时编译并存储在数据库中,执⾏速度⽐单个SQL语句快。

• 代码重⽤:存储过程可以重复调⽤,减少重复代码,提⾼代码的可维护性。

• 安全性:可以限制⽤⼾直接访问数据库,通过存储过程间接访问,从⽽保证系统安全性。

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

• 降低耦合:当表结构发⽣变化时,只需要修改相应的存储过程,应⽤程序的改动较⼩。

缺点

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

• 调试困难:只有少数数据库管理系统⽀持存储过程的调试,开发和维护困难。

• 不适合⾼并发场景:在⾼并发场景下,存储过程可能会增加数据库的压⼒,难以维护。

所以业务一般用在应用程序,而不是存储过程,不然数据库的性能压力太大了

数据库主要来处理数据的存储和检索就可以了

1.2 环境准备

bash 复制代码
-- 新建数据库
DROP DATABASE IF EXISTS topic01;
CREATE DATABASE topic01 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
use topic01;

-- 班级表
drop table if exists class;
create table class (
  id bigint primary key auto_increment,
  name varchar(20)
);

-- 学生表
drop table if exists student;
create table student (
  id bigint primary key auto_increment,
  name varchar(20) not null, 
  sno varchar(10) not null,
  age int default 18,
  gender tinyint(1), 
  enroll_date date,
  class_id bigint,
  foreign key (class_id) references class(id)
);

-- 课程表
drop table if exists course;
create table course (
  id bigint primary key auto_increment,
  name varchar(20)
);

-- 分数表
drop table if exists score;
create table score (
  id bigint primary key auto_increment,
  score float,
  student_id bigint,
  course_id bigint,
  foreign key (student_id) references student(id),
  foreign key (course_id) references course(id)
);

-- 课程表
insert into course (name) values ('Java'), ('C++'), ('MySQL'), ('操作系统'), ('计算机网络'), ('数据结构');

-- 班级表
insert into class(name) values ('Java001班'), ('C++001班'), ('前端001班');

-- 学生表
insert into student (name, sno, age, gender, enroll_date, class_id) values 
('唐三藏', '100001', 18, 1, '1986-09-01', 1),
('孙悟空', '100002', 18, 1, '1986-09-01', 1),
('猪悟能', '100003', 18, 1, '1986-09-01', 1),
('沙悟净', '100004', 18, 1, '1986-09-01', 1),
('宋江', '200001', 18, 1, '2000-09-01', 2),
('武松', '200002', 18, 1, '2000-09-01', 2),
('李逹', '200003', 18, 1, '2000-09-01', 2),
('不想毕业', '200004', 18, 1, '2000-09-01', 2);

-- 成绩表
insert into score (score, student_id, course_id) values
(70.5, 1, 1),(98.5, 1, 3),(33, 1, 5),(98, 1, 6),
(60, 2, 1),(59.5, 2, 5),
(33, 3, 1),(68, 3, 3),(99, 3, 5),
(67, 4, 1),(23, 4, 3),(56, 4, 5),(72, 4, 6),
(81, 5, 1),(37, 5, 5),
(56, 6, 2),(43, 6, 4),(79, 6, 6),
(80, 7, 2),(92, 7, 6);


-- 创建考试成绩表练习表
DROP TABLE IF EXISTS exam;
CREATE TABLE exam (
id bigint,
name VARCHAR(20),
chinese DECIMAL(4,1),
math DECIMAL(4,1),
english DECIMAL(4,1)
);
-- 插入测试数据
INSERT INTO exam (id,name, chinese, math, english) VALUES
(1,'唐三藏', 67, 98, 56),
(2,'孙悟空', 87.5, 78, 77),
(3,'猪悟能', 88, 98, 90),
(4,'曹孟德', 82, 84, 67),
(5,'刘玄德', 55.5, 85, 45),
(6,'孙权', 70, 73, 78.5),
(7,'宋公明', 75, 65, 30);

1.3 语法

创建

bash 复制代码
-- 修改SQL语句结束标识符为 //
DELIMITER //

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

-- 修改SQL语句结束标识符为 ;
DELIMITER ;

调用

bash 复制代码
CALL 存储过程名 (参数列表);

计算所有学⽣的总分

bash 复制代码
create procedure p_calAvg ()
begin
#    具体业务
    select name,chinese+math+english as total from exam;
end

这个存储过程就可以存储到数据库中了

bash 复制代码
call p_calAvg();

查看指定数据库中创建的存储过程

bash 复制代码
SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = '数据库名';
bash 复制代码
SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = 'topic01';
bash 复制代码
-- 查看存储过程的定义
SHOW CREATE PROCEDURE 存储过程名;
bash 复制代码
show create table  exam;

这个是查看表的创建

bash 复制代码
show create procedure p_calAvg;
bash 复制代码
DROP PROCEDURE [IF EXISTS] 存储过程名;
bash 复制代码
drop procedure if exists p_calAvg;
bash 复制代码
-- 修改SQL语句结束标识符为 //
DELIMITER //

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

-- 修改SQL语句结束标识符为 ;
DELIMITER ;

分号表示sql语句的结束

这样写就不知道以哪个分号结束了,其实是第一个分号结束

bash 复制代码
create procedure p_calAvg ()
begin
#    具体业务
    select name,chinese+math+english as total from exam;
end;

这样执行是会出错的,但是我们的可视化工具处理了,所以不会报错了,但是在服务器中或者命令行中就会和出错了

bash 复制代码
DELIMITER //
create procedure p_calAvg ()
begin
#    具体业务
    select name,chinese+math+english as total from exam;
end//
DELIMITER ;

这样的话,//就可以标识一个SQL语句的结束

标识一个存储过程的结束

1.4 系统变量

查看系统变量

在MySQL中变量可以分为三类:系统变量、⽤⼾⾃定义变量、以及局部变量

系统变量

系统变量是MySQL服务器的配置变量,控制着服务器的⾏为和性能。分为全局变量(GLOBAL)和会话变量(SESSION)

全局变量:启动之初的时候的变量

会话变量:每一个会话的变量,每一个链接

sql 复制代码
-- 查看所有系统变量
SHOW [GLOBAL|SESSION] VARIABLES;

-- 查看指定的系统变量
SHOW [GLOBAL|SESSION] VARIABLES LIKE 'xxx';
-- 查看指定的系统变量,可以通过LIKE进行模糊查询
SHOW [GLOBAL|SESSION] VARIABLES like '%xxx%';

-- 使用SELECT查看指定系统变量
SELECT @@[GLOBAL|SESSION].系统变量名;

-- 示例:查看以auto开头的全局系统变量
SHOW GLOBAL VARIABLES LIKE 'auto%';

-- 示例:查看以char开头的会话系统变量
SHOW SESSION VARIABLES LIKE 'char%';
-- 示例:查看事务自动提交全局系统变量
SELECT @@GLOBAL.autocommit;
sql 复制代码
show variables ;

如果不指定,默认是会话的变量

sql 复制代码
show global variables like 'auto%';
sql 复制代码
show session variables like 'auto%';

会话变量是包含全局变量的

sql 复制代码
select @@global.autocommit;

设置系统变量

sql 复制代码
SET [GLOBAL|SESSION] 系统变量名 = 值;
SET @@SESSION.系统变量名 = 值;

-- 示例:设置事务自动提交会话变量为关闭/开启
SET @@SESSION.autocommit = 0;
SET autocommit = 1;
sql 复制代码
set @@session.autocommit = 0;
select @@session.autocommit;

这个修改的只是会话级别的

全局的并没有修改,开启另一个会话,变量session.autocommit还是1

如果没有指定GLOBAL|SESSION,默认设置会话(SESSION)全局变量

• 会话关闭后,设置的会话(SESSION)变量失效;新建的会话读取全局系统变量的值做为初始值。

• MySQL重启后,设置的全局(GLOBAL)变量失效,如果想使全局系统变量永久⽣效,需要修改选项⽂件

⽤⼾⾃定义变量

⽤⼾⾃定义变量是在SQL会话中定义的变量,不⽤提前声明,作⽤域为当前会话。

赋值

sql 复制代码
-- 方式一
SET @var_name = expr [, @var_name]...;

-- 方式二 【推荐】
SET @var_name := expr [, @var_name]...;

-- 方式三: 在SELECT语句中
SELECT @var_name := expr [, @var_name]...;

-- 方式四: 查询结果赋值给自定义变量
SELECT 列名 INTO @var_name FROM 表名 WHERE ...;

作用就是保存当前查询结果

=因为既有赋值的操作,也有判断相等的操作,所以用:=

由于SQL中⽐较⽐较相等也是⽤等号 (=),所以在为变量赋值的时候推荐使⽤ ( := )

sql 复制代码
比如
select 1 =1

这个就是判断相等

使⽤

sql 复制代码
-- 示例:定义一个age变量并赋值为18,并查看
SET @age := 18;
SELECT @age;

-- 示例:从学生表中查询编号为1的学生学号并赋值给sno变量
SELECT sno INTO @sno from student where id = 1;
SELECT @sno;

-- 示例:查询学生表中的总记录数并赋值给count变量
SELECT count(*) INTO @count from student;
SELECT @count;

-- 示例:访问一个未赋值的变量,返回NULL
SELECT @var;
sql 复制代码
set @age =1;
select @age;
sql 复制代码
set @age :=2;
select @age;
sql 复制代码
select @age:=30;

这个就是赋值加查询

sql 复制代码
select count(*) into @stu_cout from student;
select @stu_cout;

一个@表示用户变量,两个@表示系统变量

sql 复制代码
select @age:=18;
select * from student where age = @age;
sql 复制代码
select @age=18;

这个就是判等了

局部变量

局部变量只在存储过程、函数或触发器的范围内有效。需要使⽤ DECLARE 声明,作⽤域的范围在声明的 BEGIN ... END 块内。

变量可以是任何有效的MySQL数据类型,如 INT 、 VARCHAR 、 DATETIME 等

声明

sql 复制代码
DECLARE 变量名 变量类型 [DEFAULT 默认值] ...;

赋值

sql 复制代码
-- 方式一
SET var_name = 值;

-- 方式二 【推荐】
SET var_name := 值;

-- 方式三: 查询结果赋值给自定义变量
SELECT 列名 INTO var_name FROM 表名 WHERE ...;

使⽤

sql 复制代码
-- 示例:在存储过程中定义局部变量记录学生表的总记录数
CREATE PROCEDURE p1()
BEGIN
    -- 定义局部变量,并指定默认值
    DECLARE stu_count INT DEFAULT 0;
    -- 把查询结果赋值给局部变量
    select count(*) into stu_count from student;
    -- 使用局部变量
    select stu_count;
END;

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

注意事项

• 变量名不区分⼤⼩写。

• 在存储过程和函数中,局部变量必须在使⽤前声明。

• ⽤⼾⾃定义变量在会话结束时失效,⽽局部变量在存储过程或函数结束时失效。

• 避免使⽤保留字作为变量名。

1.5 SQL编程

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

条件判断 - IF 语句

sql 复制代码
IF 条件1 THEN
    ......
[ELSEIF 条件2 THEN
    ......
ELSE
    ......]
END IF;

练习

• ⽰例:根据分数的值,判定当前分数对应的等级

◦ 分数 >= 90 分等级为优秀

◦ 分数 >= 80 且分数 < 90分等级为良好

◦ 分数 >= 60 分且分数 < 80分等级为及格

◦ 分数 < 60分等级为不及格

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p2()
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;

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

参数

类型 描述

IN 输⼊类型,调⽤存储过程时要传⼊的值,默认参数类型,是值类型,修改了就修改了,外部变量不会改变

OUT 输出类型,可以作为存储过程的返回值,专门来存返回值的变量,不修改

INOUT 输⼊输出类型,即可以作为输⼊类型也可以做为输出类型,是引用类型,内部修改了,外部也会修改

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

-- 创建存储过程
CREATE PROCEDURE 存储过程名 ([IN/OUT/INOUT 参数名 参数类型] [,...])
BEGIN
    -- SQL 语句
END //

-- 修改SQL语句结束标识符为 ;
DELIMITER ;

练习

• ⽰例1:传⼊⼀个分数的值,判定当前分数对应的等级

◦ 分数 >= 90 分等级为优秀

◦ 分数 >= 80 且分数 < 90分等级为良好

◦ 分数 >= 60 分且分数 < 80分等级为及格

◦ 分数 < 60分等级为不及格

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p3(IN score INT, OUT result VARCHAR(10))
BEGIN
    -- 判断
    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;

END;

-- 调用存储过程
CALL p3 (88, @result);

-- 查看结果
SELECT @result;

@result是用户自定义变量

⽰例2:传⼊⼀个分数的值,在传⼊分数的基础上加10分,然后返回

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p4(INOUT score INT)
BEGIN
    -- 在原分数的基础上加10分
    SET score := score + 10;
END;

SET @score := 98;
-- 调用存储过程
CALL p4 (@score);

-- 查看结果
SELECT @score;

SQL中没有+=的操作

case语句

语法⼀

• CASE 后的 case_value 是⼀个表达式,该表达式的值与每⼀个 WHEN ⼦句中的when_value ⽐较,当找到⼀个相等的 when_value 时,执⾏相应的 THEN ⼦句的statement_list

如果没有相等的 when_value ,则执⾏ ELSE ⼦句 statement_list (如果存在ELSE)。

sql 复制代码
-- 语法一
CASE case_value
    WHEN when_value THEN statement_list
    [WHEN when_value THEN statement_list] ...
    [ELSE statement_list]
END CASE

语法⼆

• 计算每个 WHEN ⼦句 search_condition 表达式,直到其中⼀个表达式为真,此时执⾏相应

的 THEN ⼦句的 statement_list 。

• 如果 search_condition 都不相等,则执⾏ ELSE ⼦句 statement_list (如果存在

ELSE)。

sql 复制代码
-- 语法二
CASE
    WHEN search_condition THEN statement_list
    [WHEN search_condition THEN statement_list] ...
    [ELSE statement_list]
END CASE

注意:

• 每个 statement_list 由⼀条或多条SQL语句组成且不允许为空。但可以使⽤BEGIN... END;块。

• 如果没有 when_value 或 search_condition 与测试值匹配,并且 CASE 语句不包含ELSE ⼦句,则会导致CASE语句错误

练习

• ⽰例⼀:传⼊⼀个状态码,输出该状态码表⽰的含义

◦ 0:成功

◦ 10001:⽤⼾名或密码错误

◦ 10002:您没有对应的权限,请联系管理员

◦ 20001:你传⼊的参数有误

◦ 20002:没有找到相应的结果

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p5(IN code INT, OUT result varchar(50))
BEGIN
    CASE code
        WHEN 0 THEN
            SET result := '成功';
        WHEN 10001 THEN
            SET result := '用户名或密码错误';
        WHEN 10002 THEN
            SET result := '您没有对应的权限';
        WHEN 20001 THEN
            SET result := '你传入的参数有误';
        WHEN 20002 THEN
            SET result := '没有找到相应的结果';
        ELSE
            SET result := '服务器错误,请联系管理员';
    END CASE;
END;

-- 调用存储过程
CALL p5(10001, @result);
-- 查看结果
SELECT @result;

⽰例⼆:根据传⼊的⽉份,输出该⽉份属于哪个季度

◦ 1 ~ 3⽉为第⼀季度

◦ 4 ~ 6⽉为第⼀季度

◦ 7 ~ 9⽉为第⼀季度

◦ 1 ~ 12⽉为第⼀季度

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p6(IN month INT, OUT result varchar(50))
BEGIN
    CASE
        WHEN month >= 1 AND month <= 3 THEN
            SET result := '第一季度';
        WHEN month >= 4 AND month <= 6 THEN
            SET result := '第二季度';
        WHEN month >= 7 AND month <= 9 THEN
            SET result := '第三季度';
        WHEN month >= 10 AND month <= 12 THEN
            SET result := '第四季度';
        ELSE
            SET result := '非法参数';
    END CASE;

END;

-- 调用存储过程
CALL p6(6, @result);
-- 查询结果
SELECT @result;

循环

WHILE

语法

• 先判断条件表达式 search_condition 是否为 TRUE ,如果条件成⽴,则执⾏循环体中的statement_list

sql 复制代码
WHILE search_condition DO
    statement_list
END WHILE;

练习

• ⽰例:传⼊⼀个数n,计算从1累加到n的值

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p7(IN n INT)
BEGIN
    -- 定义一个变量,表示结果
    DECLARE total int DEFAULT 0;

    -- 累加
    WHILE n > 0 DO
        SET total := total + n;
        SET n := n - 1;
    END WHILE;

    -- 查询结果
    SELECT total;

END;

-- 调用存储过程
CALL p7(100);

REPEAT

语法

• 先执⾏⼀次循环体中的 statement_list ,再先判断条件表达式 search_condition 是否

为 TRUE ,如果条件成⽴,则继续执⾏循环体中的语句,如果条件不成⽴则退出循环

• statement_list ⾄少会执⾏⼀次,类似与C和JAVA中的do ... while 循环

sql 复制代码
REPEAT
    statement_list
UNTIL search_condition
END REPEAT;

练习

• ⽰例:传⼊⼀个数n,计算从1累加到n的值

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p8(IN n INT)
BEGIN
    -- 定义一个变量,表示结果
    DECLARE total int DEFAULT 0;

    -- 累加
    REPEAT
        SET total := total + n;
        SET n := n - 1;
    UNTIL n <= 0 END REPEAT;

    -- 查询结果
    SELECT total;

END;

-- 调用存储过程
CALL p8(100);

LOOP

• LOOP也可以实现⼀个简单的循环,并且当满⾜某个条件时终⽌当前循环或退出整个循环,通常配

合以下两个⼦句使⽤

◦ LEAVE label :退出整个循环,类似于C++或JAVA中的 break;

◦ ITERATE label : 终⽌当前循环,进⼊下⼀次循环,类似于C++或JAVA中的 continue;

语法

sql 复制代码
[begin_label:] LOOP
	statement_list
END LOOP [end_label]

练习

• ⽰例:传⼊⼀个数n,计算从1累加到n的值

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p9(IN n INT)
BEGIN
    -- 定义一个变量,表示结果
    DECLARE total int DEFAULT 0;

    -- 累加
    sum_label: LOOP
        -- 判断是否退出
        IF n <= 0 THEN
            LEAVE sum_label;
        END IF;

        -- 累加操作
        SET total := total + n;
        SET n := n - 1;
    END LOOP sum_label;

    -- 查询结果
    SELECT total;

END;

-- 调用存储过程
CALL p9(100);

• ⽰例:传⼊⼀个数n,累加从1累加到n之间偶数的值

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p10(IN n INT)
BEGIN
    -- 定义一个变量,表示结果
    DECLARE total int DEFAULT 0;

    -- 累加
    sum_label: LOOP
        -- 判断是否退出
        IF n <= 0 THEN
            LEAVE sum_label;
        END IF;

        -- 判断是否偶数
        IF n % 2 = 1 THEN
            SET n := n - 1;
            -- 跳出本次循环
            ITERATE sum_label;
        END IF;

        -- 累加操作
        SET total := total + n;
        SET n := n - 1;

    END LOOP sum_label;

    -- 查询结果
    SELECT total;

END;

-- 调用存储过程
CALL p10(100);

1.6 游标

MySQL中的游标是⼀种数据库对象,允许在存储过程和函数中对查询到的结果集进⾏逐⾏检索。

• ⽐如在以下结果集中,可以通过向前移动游标,实现每⼀⾏的数据读取

注意:MySQL的游标是只读的,不能进⾏更新操作。

语法

• 使⽤游标之前必须先声明游标,之后使⽤ OPEN 、 FETCH 和 CLOSE 语句来打开游标、获取游标

记录和关闭游标。

• 游标必须在条件处理程序之前被声明,并且变量必须在游标或条件处理程序之前被声明。

sql 复制代码
-- 声明游标
DECLARE 游标名 CURSOR FOR 查询语句;
-- 打开游标,真正的指向查询语句,并把游标和查询语句关联在一起
OPEN 游标名;
-- 获取游标记录
FETCH 游标名 INTO 变量[, 变量] ...;
-- 关闭游标
CLOSE 游标名;

练习

• ⽰例:传⼊班级编号,查询学⽣表中属于该班级的学⽣信息,并将符合条件的学⽣信息写⼊到⼀张

新表中

◦ 新表及字段t_student_class (id, student_name, class_name)

• 实现逻辑

◦ 定义变量⽤于接收查询结果集中每⼀⾏中列的值

◦ 声明游标,⽤于接收查询结果集

◦ 创建新表

◦ 开启游标

◦ 从游标中获取结果集中的记录

◦ 向新表中写⼊数据

◦ 关闭游标

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p11(IN class_id INT)
BEGIN
    -- 首先声明变量
    -- 定义学生姓名变量
    DECLARE student_name VARCHAR(20);
    -- 定义班级名变量
    DECLARE class_name VARCHAR(20);

    -- 定义游标
    DECLARE s_cursor CURSOR FOR 
        SELECT s.`name` student_name, c.`name` class_name 
        FROM student s, class c 
        WHERE s.class_id = c.id AND s.class_id = class_id;

    -- 创建新表
    DROP TABLE IF EXISTS t_student_class;
    CREATE TABLE IF NOT EXISTS t_student_class (
        id BIGINT PRIMARY KEY AUTO_INCREMENT,
        student_name VARCHAR(20),
        class_name VARCHAR(20)
    );

    -- 开启游标
    OPEN s_cursor;
    -- 遍历结果集
    WHILE TRUE DO
        -- 获取游标记录
        FETCH s_cursor INTO student_name, class_name;
        -- 写入新表
        INSERT INTO t_student_class VALUES (NULL, student_name, class_name);
    END WHILE;

    -- 关闭游标
    CLOSE s_cursor;

END;

-- 调用存储过程(会出错)
CALL p11(1);
-- 错误信息:
-- ERROR 1329 (02000): No data - zero rows fetched, selected, or processed

由于while循环的退出条件是true,此时是⼀个死循环,当游标遍历完成之后继续向后遍历,发现没有记录,所以报错,可以通过条件处理程序解决

1.7 条件处理程序

定义条件是事先定义程序执⾏过程中可能遇到的问题

• 处理程序定义了在遇到问题时应当采取的处理⽅式

• 使⽤条件处理程序保证存储过程或函数在遇到警告或错误时能继续执⾏,可以增强程序处理问题的能⼒,避免程序异常停⽌运⾏。

sql 复制代码
DECLARE handler_action HANDLER
    FOR condition_value [, condition_value] ...
    statement

handler_action: {
    CONTINUE  -- 继续执行当前程序
  | EXIT      -- 终止执行当前程序
}

condition_value: {
    mysql_error_code  -- MYSQL错误码
  | SQLSTATE [VALUE] sqlstate_value  -- 状态码
  | SQLWARNING  -- 所有以01开头的SQLSTATE代码
  | NOT FOUND   -- 所有以02开头的SQLSTATE代码
  | SQLEXCEPTION  -- 所有没有被SQLWARNING或NOT FOUND捕获的SQLSTATE代码
}

练习

• ⽰例:加⼊条件处理程序,解决游标越界问题

sql 复制代码
-- 创建存储过程
CREATE PROCEDURE p12(IN class_id INT)
BEGIN
    -- 首先声明变量
    -- 定义学生姓名变量
    DECLARE student_name VARCHAR(20);
    -- 定义班级名变量
    DECLARE class_name VARCHAR(20);
    -- 定义游标结束标识
    DECLARE is_done bool DEFAULT FALSE;

    -- 定义游标
    DECLARE s_cursor CURSOR FOR select s.`name` student_name, c.`name` class_name from student s, class c where s.class_id = c.id and s.class_id = class_id;
    -- 定义条件处理程序
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET is_done := TRUE;

    -- 创建新表
    DROP TABLE IF EXISTS t_student_class;
    CREATE TABLE IF NOT EXISTS t_student_class (
        id BIGINT PRIMARY KEY AUTO_INCREMENT,
        student_name VARCHAR(20),
        class_name VARCHAR(20)
    );

    -- 开启游标
    OPEN s_cursor;

    -- 遍历结果集
    read_loop: LOOP
        -- 获取游标记录
        FETCH s_cursor INTO student_name, class_name;
        -- 退出循环
        IF is_done THEN
            LEAVE read_loop;
        END IF;

        -- 写入到新表
        INSERT INTO t_student_class VALUES (NULL, student_name,class_name);
        SELECT student_name;
    END LOOP read_loop;

    -- 关闭游标
    CLOSE s_cursor;

END;

-- 调用存储过程
CALL p12(1);

游标必须在条件处理程序之前被声明,并且变量必须在游标或条件处理程序之前被声明

sql 复制代码
DECLARE CONTINUE HANDLER FOR NOT FOUND SET is_done := TRUE;

CONTINUE 表示错误发生之后继续执行代码, NOT FOUND 表示发生了 NOT FOUND 这种错误的状态码的时候就去执行set后面的语句

1.8 存储函数

MySQL存储函数是有返回值的存储过程,参数只能是IN类型,类似于内置函数。存储函数与存储过程的主要区别在于存储函数必须有返回值,⽽存储过程则不⼀定。

语法

• 在MySQL 8.0版本中,如果binlog是开启的,那么在定义存储函数时,需要指定characteristic特性,否则会报错

sql 复制代码
CREATE FUNCTION 存储函数名称 ([参数列表])
RETURNS type [characteristic ...]
BEGIN
    -- SQL语句
    RETURN ...;
END;

characteristic:
  [NOT] DETERMINISTIC --表示相同的输入参数总是产生[不同]相同的结果
| NO SQL --不包含SQL语句
| READS SQL DATA --包含读取数据的语句,如select
| MODIFIES SQL DATA -- 包含写入数据的语句,如update,delete

-- 使用存储函数
select 存储函数名称 ([参数列表]);

练习

• ⽰例:传⼊⼀个数n,计算从1累加到n的值

sql 复制代码
-- 定义存储函数
CREATE FUNCTION fun1(n INT)
RETURNS INT
BEGIN
-- 定义一个变量,表示结果
DECLARE total int DEFAULT 0;

-- 累加
WHILE n > 0 DO
  SET total := total + n;
  SET n := n - 1;
END WHILE;

-- 返回结果
RETURN total;

END;

-- 1418 - This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)
sql 复制代码
-- 定义存储函数
CREATE FUNCTION fun1(n INT)
RETURNS INT DETERMINISTIC  -- 指定characteristic
BEGIN
-- 定义一个变量,表示结果
DECLARE total int DEFAULT 0;

-- 累加
WHILE n > 0 DO
  SET total := total + n;
  SET n := n - 1;
END WHILE;

-- 查询结果
RETURN total;
END;

-- 调用存储函数
select fun1(100);

1.9 触发器

触发器是什么

• 触发器是⼀个与表关联的数据库对象,在对表进⾏insert、update、delete操作时,触发并执⾏定

义触发器时指定的SQL语句。

• 触发器可以在对表操作之前或之后执⾏,这被称为触发时间。

• 触发器可以执⾏SQL语句或逻辑块,⽤于实施复杂的业务逻辑或数据验证。

• MySQL⽀持三种类型的触发器:INSERT触发器、UPDATE触发器和DELETE触发器。使⽤ OLD

和 NEW 关键字来引⽤触发器中发⽣变化的记录内容。

触发器类型 OLD和NEW

INSERT触发器 NEW表⽰将要或已经新增的数据

UPDATE触发器 OLD表⽰修改之前的数据,NEW表⽰将要或已经修改的数据

DELETE触发器 OLD表⽰将要或已经删除的数据

⾏级触发器和语句级触发器:

• ⾏级触发器:当对表中的每⼀⾏进⾏INSERT、UPDATE或DELETE操作时,⾏级触发器都会被触发。例如,如果执⾏⼀个UPDATE语句影响了多⾏数据,那么⾏级触发器会对每⼀⾏都触发⼀次。⾏级触发器可以访问受影响⾏的旧值和新值,常⽤于实现复杂的业务逻辑时对新旧值的访问。

• 语句级触发器:在整个INSERT、UPDATE或DELETE语句执⾏时只触发⼀次。⽆论该语句影响了多少⾏数据,语句级触发器都只在语句开始或结束时触发⼀次。语句级触发器主要⽤于实现⼀些全局性的操作,⽐如数据同步、数据清理等。

• MySQL只⽀持⾏级触器,不⽀持语句级触发器。

语法

sql 复制代码
-- 创建
CREATE TRIGGER [IF NOT EXISTS] trigger_name
    trigger_time trigger_event
    ON tbl_name FOR EACH ROW
BEGIN
    trigger_stmt;
END;

trigger_time: { BEFORE | AFTER }

trigger_event: { INSERT | UPDATE | DELETE }

-- 查看所有触发器
SHOW TRIGGERS;

-- 删除,如果没有指定schema_name,默认为当前数据库
DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name;

FOR EACH ROW表示是行级触发器

练习

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

除操作。

sql 复制代码
-- 创建学生日志表
create table student_log (
    id bigint primary key auto_increment,

    operation_type varchar(10) not null comment '操作类型: insert/update/delete',
    operation_time datetime not null comment '操作时间',
    operation_id bigint not null comment '操作的记录ID',
    operation_data varchar(500) comment '操作数据'
);

插⼊数据的触发器

sql 复制代码
-- 插入触发器
create trigger trg_student_insert
    after insert on student for each row
begin
    -- 插入新增数据日志
    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;

-- 向学生表中插入记录
insert into student values (null, '曹操', '300001', 28, 1, '2024-09-01', 3);

-- 查看学生日志表
select id, operation_type as type, operation_time as time, operation_id as op_id, operation_data as data from student_log;

更新数据的触发器

sql 复制代码
-- 更新触发器
create trigger trg_student_update
    after update on student for each row
begin
    -- 插入新增数据日志
    insert into student_log (
        operation_type,
        operation_time,
        operation_id,
        operation_data)
    values (
        'update',
        now(),
        new.id,
        concat(old.id, ',', old.name, ',', old.sno, ',', old.age, ',',
        old.gender, ',', old.enroll_date, ',', old.class_id,
        '|', new.id, ',', new.name, ',', new.sno, ',', new.age, ',',
        new.gender, ',', new.enroll_date, ',', new.class_id)
    );
end;

-- 更新学生表中记录
update student set age = 20, class_id = 2 where name = '曹操';

-- 查看学生日志表
select * from student_log;

-- 2 | update | 2024-09-19 11:47:21 | 13 | 13,曹操,300001,28,1,2024-09-01,3|13,曹操,300001,20,1,2024-09-01,2

-- 更新多条记录
update student set class_id = 3 where id >= 7;

删除数据的触发器

sql 复制代码
-- 删除触发器
create trigger trg_student_delete
    after delete on student for each row
begin
    -- 插入新增数据日志
    insert into student_log (
        operation_type,
        operation_time,
        operation_id,
        operation_data)
    values (
        'delete',
        now(),
        old.id,
        concat(old.id, ',', old.name, ',', old.sno, ',', old.age, ',',
        old.gender, ',', old.enroll_date, ',', old.class_id)
    );
end;

-- 删除学生表中记录
delete from student where name = '曹操';

-- 查看学生日志表
select * from student_log;

1.10 常见面试题

本节可以解决的问题

  1. 存储过程的作⽤是什么?

    完成特定功能的SQL语句集,类似于java中的方法,在数据库层面实现的。对应用程序没有影响

    编译后存储在数据库中,⽤⼾通过指定存储过程的名字和参数来执⾏,并获取相应的结果。

  2. 如何创建⼀个存储过程?

  3. MySQL中的变量都有哪⼏种?

  4. 如何定义⼀个变量?

  5. MySQL中使⽤变量是否需要提前声明?

    局部变量需要声明

  6. MySQL中的参数分为哪⼏种?

    输入类型,输出类型

  7. ⽤过游标吗?游标的作⽤是什么?

    遍历存储过程和函数中查询到的结果集

  8. 了解条件处理程序吗?介绍⼀下如何使⽤

    触发条件(可能遇到的问题),和触发方式,continue是继续执行,exit是直接退出存储过程,游标必须在条件处理程序之前执行,变量最先声明

  9. 存储函数与存储过程的区别是什么?

    存储函数必须有返回值,而且是return的方式返回,还要定义returns的类型,8.0之后还要指定特性,参数只能是输入类型的参数

  10. 如何查看数据库中创建的存储过程?

  11. 什么是触发器?

    与表关联的数据库对象,对表进行操作的时候,会执行触发器的相关SQL语句

  12. MySQL中触发器分为⼏种类型?

    insert,update,delete

  13. ⾏级触发器与语句级触器的区别是什么?

    表中每一行对修改都会触发,影响多少行就触发多少个,一个语句,不管影响多少行,那么都执行一次,mysql不支持语句级触发

  14. 说⼀下了解的触发器使⽤场景都有哪些?

    修改之前触发,修改之后触发,之后触发·记录日志

  15. 如果对⼀表中的数据进⾏更新,要在⽇志表中记录该条记录更新前与更新后的值,如何实现?

  16. 如何查看数据库中创建的触发器?

总结

相关推荐
小高不会迪斯科10 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
e***89011 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
l1t11 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
冬奇Lab12 小时前
Android系统启动流程深度解析:从Bootloader到Zygote的完整旅程
android·源码阅读
失忆爆表症12 小时前
03_数据库配置指南:PostgreSQL 17 + pgvector 向量存储
数据库·postgresql
AI_567812 小时前
Excel数据透视表提速:Power Query预处理百万数据
数据库·excel
SQL必知必会13 小时前
SQL 窗口帧:ROWS vs RANGE 深度解析
数据库·sql·性能优化
泓博13 小时前
Android中仿照View selector自定义Compose Button
android·vue.js·elementui
Gauss松鼠会14 小时前
【GaussDB】GaussDB数据库开发设计之JDBC高可用性
数据库·数据库开发·gaussdb
+VX:Fegn089514 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计