【第1章】 存储过程
1. 存储过程的概念
存储过程可称为过程化SQL语言,是在普通SQL语句的基础上增加了编程语言的特点,把数据操作语句(DML)和查询语句(DQL)组织在过程化代码中,通过逻辑判断、循环等操作实现复杂计算的程序语言。
换句话说,存储过程其实就是数据库内置的一种编程语言,这种编程语言也有自己的变量、if语句、循环语句等。在一个存储过程中可以将多条SQL语句以逻辑代码的方式将其串联起来,执行这个存储过程就是将这些SQL语句按照一定的逻辑去执行,所以一个存储过程也可以看做是一组为了完成特定功能的SQL 语句集。
每一个存储过程都是一个数据库对象,就像table和view一样,存储在数据库当中,一次编译永久有效。并且每一个存储过程都有自己的名字。客户端程序通过存储过程的名字来调用存储过程。
在数据量特别庞大的情况下利用存储过程能达到倍速的效率提升。
2. 存储过程的优点和缺点
优点:速度快。
- 降低了应用服务器和数据库服务器之间网络通讯的开销。尤其在数据量庞大的情况下效果显著。
缺点:移植性差。编写难度大。维护性差。
- 每一个数据库都有自己的存储过程的语法规则,这种语法规则不是通用的。一旦使用了存储过程,则数据库产品很难更换,例如:编写了mysql的存储过程,这段代码只能在mysql中运行,无法在oracle数据库中运行。
- 对于数据库存储过程这种语法来说,没有专业的IDE工具(集成开发环境),所以编码速度较低。自然维护的成本也会较高。
在实际开发中,存储过程还是很少使用的。只有在系统遇到了性能瓶颈,在进行优化的时候,对于大数量的应用来说,可以考虑使用一些。
3. 第一个存储过程
3.1 存储过程的创建
1 | create procedure p1() |
3.2 存储过程的调用
1 | call p1(); |
3.3 存储过程的查看
查看创建存储过程的语句:
1 | show create procedure p1; |
通过系统表information_schema.ROUTINES
查看存储过程的详细信息:
information_schema.ROUTINES
是 MySQL 数据库中一个系统表,存储了所有存储过程、函数、触发器的详细信息,包括名称、返回值类型、参数、创建时间、修改时间等。
1 | select * from information_schema.routines where routine_name = 'p1'; |
information_schema.ROUTINES
表中的一些重要的列包括:
- SPECIFIC_NAME:存储过程的具体名称,包括该存储过程的名字,参数列表。
- ROUTINE_SCHEMA:存储过程所在的数据库名称。
- ROUTINE_NAME:存储过程的名称。
- ROUTINE_TYPE:PROCEDURE表示是一个存储过程,FUNCTION表示是一个函数。
- ROUTINE_DEFINITION:存储过程的定义语句。
- CREATED:存储过程的创建时间。
- LAST_ALTERED:存储过程的最后修改时间。
- DATA_TYPE:存储过程的返回值类型、参数类型等。
3.4 存储过程的删除
1 | drop procedure if exists p1; |
3.5 delimiter命令
在 MySQL 中,delimiter
命令用于改变 MySQL 解释语句的定界符。MySQL 默认使用分号 ;
作为语句的定界符。而使用 delimiter
命令可以将分号 ;
更改为其他字符,从而可以在 SQL 语句中使用分号 ;
例如,假设需要创建一个存储过程。在存储过程中通常会包括多条 SQL 语句,而这些语句都需要以分号 ;
结尾。但默认情况下,执行到第一条语句的分号 ;
后,MySQL 就会停止解释,导致后续的语句无法执行。解决方式就是使用 delimiter
命令将分号改为其他字符,使分号 ;
不再是语句定界符。
1 | delimiter // |
在这个例子中,我们使用 delimiter //
命令将定界符改为两个斜线 //
。在存储过程中,以分号 ;
结尾的语句不再被解释为语句的结束。而使用 delimiter ;
可以将分号恢复为语句定界符。
总之,delimiter
命令可以改变 MySQL 数据库系统中 SQL 查询语句的分隔符,从而可使一条 SQL 查询语句包含多个 SQL 语句。这样的话,就方便了我们在一个语句里面加入多个语句,而且不会被错
4. MySQL的变量
mysql中的变量包括:系统变量、用户变量、局部变量。
4.1 系统变量
MySQL 系统变量是指在 MySQL 服务器运行时控制其行为的参数。这些变量可以被设置为特定的值来改变服务器的默认设置,以满足不同的需求。
MySQL 系统变量可以具有全局(global)或会话(session)作用域。
- 全局作用域是指对所有连接和所有数据库都适用;
- 会话作用域是指只对当前连接和当前数据库适用。
查看系统变量:
1 | show [global|session] variables; |
PS:没有指定session或global时,默认是session
设置系统变量:
1 | set [global | session] 系统变量名 = 值; |
PS:无论是全局设置还是会话设置,当mysql服务重启之后,之前配置都会失效。可以通过修改MySQL根目录下的my.ini配置文件达到永久修改的效果。(my.ini是MySQL数据库默认的系统级配置文件,默认是不存在的,需要新建,并参考一些资料进行配置。)
windows系统是my.ini
,linux系统是my.cnf
,my.ini
文件通常放在mysql安装的根目录下。
这个文件通常是不存在的,可以新建,新建后例如提供以下配置:
1 | [mysqld] |
这种配置就表示永久性关闭自动提交机制。(不建议这样做。)
4.2 用户变量
用户自定义的变量。只在当前会话有效。所有的用户变量@
开始。
给用户变量赋值:
1 | set @name = 'jackson'; |
读取用户变量的值:
1 | select @name, @age, @gender, @addr, @email, @sal; |
PS:mysql中变量不需要声明。直接赋值就行。如果没有声明变量,直接读取该变量,返回null
4.3 局部变量
在存储过程中可以使用局部变量。使用declare声明。在begin和end之间有效。
变量的声明:
1 | declare 变量名 数据类型 [default ...]; |
变量的数据类型就是表字段的数据类型,例如:int、bigint、char、varchar、date、time、datetime等。
PS:declare通常出现在begin end之间的开始部分。
变量的赋值:
1 | set 变量名 = 值; |
例如:以下程序演示局部变量的声明、赋值、读取:
1 | create PROCEDURE p2() |
1 | call p2(); |
5. if语句
语法格式:
1 | if 条件 then |
案例:员工月薪sal,超过10000的属于“高收入”,6000到10000的属于“中收入”,少于6000的属于“低收入”
1 | create procedure p3( ) |
1 | call p3(); |
6. 参数
存储过程的参数包括三种形式:
- in:入参(未指定时,默认是in)
- out:出参
- inout:既是入参,又是出参
案例:员工月薪sal,超过10000的属于“高收入”,6000到10000的属于“中收入”,少于6000的属于“低收入”
1 | create procedure p4(in sal int, out grade varchar(20)) |
1 | call p4(5000, @grade); |
案例:将传入的工资sal上调10%
1 | create procedure p5(inout sal int) |
1 | set @sal := 10000; |
7. case语句
语法格式:
1 | case 值 |
1 | case |
案例:根据不同月份,输出不同的季节。3、4、5月份春季。6、7、8月份夏季。9、10、11月份秋季。12、1、2 冬季。其他非法
1 | create procedure mypro(in month int, out result varchar(100)) |
1 | create procedure mypro(in month int, out result varchar(100)) |
1 | call mypro(9, @season); |
8. while循环
语法格式:
1 | while 条件 do |
案例:传入一个数字n,计算1~n中所有偶数的和
1 | create procedure mypro(in n int) |
1 | call mypro(10); |
9. repeat循环
语法格式:
1 | repeat |
PS:条件成立时结束循环
案例:传入一个数字n,计算1~n中所有偶数的和
1 | create procedure mypro(in n int, out sum int) |
1 | call mypro(10, @sum); |
10. loop循环
语法格式:
1 | create procedure mypro() |
案例
1 | create procedure mypro() |
11. 游标cursor
游标(cursor)可以理解为一个指向结果集中某条记录的指针,允许程序逐一访问结果集中的每条记录,并对其进行逐行操作和处理。
使用游标时,需要在存储过程或函数中定义一个游标变量,并通过 DECLARE
语句进行声明和初始化。然后,使用 OPEN
语句打开游标,使用 FETCH
语句逐行获取游标指向的记录,并进行处理。最后,使用 CLOSE
语句关闭游标,释放相关资源。游标可以大大地提高数据库查询的灵活性和效率。
声明游标的语法:
1 | declare 游标名称 cursor for 查询语句; |
打开游标的语法:
1 | open 游标名称; |
通过游标取数据的语法:
1 | fetch 游标名称 into 变量[,变量,变量......] |
关闭游标的语法:
1 | close 游标名称; |
案例:从dept表查询部门编号和部门名,创建一张新表dept2,将查询结果插入到新表中
1 | drop procedure if exists mypro; |
执行结果:
出现了异常:异常信息中显示没有数据了。这是因为while true循环导致的。
不过虽然出现了异常,但是表创建成功了,数据也插入成功了:
注意:声明局部变量和声明游标有顺序要求,局部变量的声明需要在游标声明之前完成。
12. 捕捉异常并处理
语法格式:
1 | DECLARE handler_name HANDLER FOR condition_value action_statement |
handler_name
表示异常处理程序的名称,重要取值包括:CONTINUE
:发生异常后,程序不会终止,会正常执行后续的过程。(捕捉)EXIT
:发生异常后,终止存储过程的执行。(上抛)
condition_value
是指捕获的异常,重要取值包括:SQLSTATE sqlstate_value
,例如:SQLSTATE ‘02000’SQLWARNING
,代表所有01开头的SQLSTATENOT FOUND
,代表所有02开头的SQLSTATESQLEXCEPTION
,代表除了01和02开头的所有SQLSTATE
action_statement
是指异常发生时执行的语句,例如:CLOSE cursor_name
给之前的游标添加异常处理机制:
1 | drop procedure if exists mypro; |
13. 存储函数
存储函数:带返回值的存储过程。参数只允许是in(但不能写显示的写in)。没有out,也没有inout。
语法格式:
1 | CREATE FUNCTION 存储函数名称(参数列表) RETURNS 数据类型 [特征] |
“特征”的可取重要值如下:
deterministic
:用该特征标记该函数为确定性函数(什么是确定性函数?每次调用函数时传同一个参数的时候,返回值都是固定的)。这是一种优化策略,这种情况下整个函数体的执行就会省略了,直接返回之前缓存的结果,来提高函数的执行效率。no sql
:用该特征标记该函数执行过程中不会查询数据库,如果确实没有查询语句建议使用。告诉 MySQL 优化器不需要考虑使用查询缓存和优化器缓存来优化这个函数,这样就可以避免不必要的查询消耗产生,从而提高性能。reads sql data
:用该特征标记该函数会进行查询操作,告诉 MySQL 优化器这个函数需要查询数据库的数据,可以使用查询缓存来缓存结果,从而提高查询性能;同时 MySQL 还会针对该函数的查询进行优化器缓存处理。
案例:计算1~n的所有偶数之和
1 | -- 删除函数 |
14. 触发器
MySQL 触发器是一种数据库对象,它是与表相关联的特殊程序。它可以在特定的数据操作(例如插入(INSERT)、更新(UPDATE)或删除(DELETE))触发时自动执行。MySQL 触发器使数据库开发人员能够在数据的不同状态之间维护一致性和完整性,并且可以为特定的数据库表自动执行操作。
触发器的作用主要有以下几个方面:
- 强制实施业务规则:触发器可以帮助确保数据表中的业务规则得到强制执行,例如检查插入或更新的数据是否符合某些规则。
- 数据审计:触发器可以声明在执行数据修改时自动记日志或审计数据变化的操作,使数据对数据库管理员和 SQL 审计人员更易于追踪和审计。
- 执行特定业务操作:触发器可以自动执行特定的业务操作,例如计算数据行的总数、计算平均值或总和等。
MySQL 触发器分为两种类型: BEFORE
和 AFTER
。BEFORE 触发器在执行 INSERT、UPDATE、DELETE 语句之前执行,而 AFTER 触发器在执行 INSERT、UPDATE、DELETE 语句之后执行。
创建触发器的语法如下:
1 | CREATE TRIGGER trigger_name |
其中:
trigger_name
:触发器的名称BEFORE/AFTER
:触发器的类型,可以是 BEFORE 或者 AFTERINSERT/UPDATE/DELETE
:触发器所监控的 DML 调用类型table_name
:触发器所绑定的表名FOR EACH ROW
:表示触发器在每行受到 DML 的影响之后都会执行- 触发器执行的 SQL 语句:该语句会在触发器被触发时执行
需要注意的是,触发器是一种高级的数据库功能,只有在必要的情况下才应该使用,例如在需要实施强制性业务规则时。过多的触发器和复杂的触发器逻辑可能会影响查询性能和扩展性。
关于触发器的NEW和OLD关键字:
在 MySQL 触发器中,NEW
和 OLD
是两个特殊的关键字,用于引用在触发器中受到修改的行的新值和旧值。具体而言:
NEW
:在触发 INSERT 或 UPDATE 操作期间,NEW 用于引用将要插入或更新到表中的新行的值。OLD
:在触发 UPDATE 或 DELETE 操作期间,OLD 用于引用更新或删除之前在表中的旧行的值。
通俗的讲,NEW 是指触发器执行的操作所要插入或更新到当前行中的新数据;而 OLD 则是指当前行在触发器执行前原本的数据。
在MySQL 触发器中,NEW 和 OLD 使用方法是相似的。在触发器中,可以像引用表的其他列一样引用 NEW 和 OLD。例如,可以使用 OLD.column_name 从旧行中引用列值,也可以使用 NEW.column_name 从新行中引用列值。
示例
假设有一个名为 my_table 的表,其中包含一个名为 quantity 的列。当在该表上执行 UPDATE 操作时,以下触发器会将旧值 OLD.quantity 累加到新值 NEW.quantity 中:
1 | CREATE TRIGGER my_trigger |
在此触发器中,OLD.quantity 引用原始行的 quantity 值(旧值),而 NEW.quantity 引用更新行的 quantity 值(新值)。在触发器执行期间,数据行的 quantity 值将设置为旧值加上新值。
需要注意的是,在使用 NEW 和 OLD 时,需要根据 DML 操作的类型进行判断,以确定哪个关键字表示新值,哪个关键字则表示旧值。
案例:当我们对dept表中的数据进行insert、delete、update的时候,请将这些操作记录到日志表当中
日志表如下:
1 | drop table if exists oper_log; |
触发器1:向dept表中插入数据时,记录日志
1 | create trigger dept_trigger_insert |
查看触发器:
1 | show triggers; |
删除触发器:
1 | drop trigger if exists dept_trigger_insert; |
向dept表中插入一条记录:
日志表中多了一条记录:
触发器2:修改dept表中数据时,记录日志
1 | create trigger dept_trigger_update |
更新一条记录:
1 | update dept set loc = '北京' where deptno = 60; |
日志表中多了一条记录:
PS:更新一条记录则对应一条日志。如果一次更新3条记录,那么日志表中插入3条记录。
触发器3:删除dept表中数据时,记录日志
1 | create trigger dept_trigger_delete |
删除一条记录:
1 | delete from dept where deptno = 60; |
日志表中多了一条记录: