前言
存储过程可以理解为数据库里的 SQL 代码块。它把一组 SQL 语句提前定义好,后续通过一次调用执行。这样做的好处是:重复逻辑可以复用,应用层不用每次拼一大段 SQL,多次数据库交互也可以压缩成一次调用。
学习存储过程不要只背语法,更重要的是看清一条链路:先创建并调用过程,再给过程传参,接着在过程内部使用变量和流程控制,最后用游标处理多行结果集。掌握这条链路后,存储函数只是"有返回值的存储过程"的延伸。
一、基本语法:先把 SQL 封装起来
最简单的存储过程没有参数,只是把一段 SQL 包在过程体中:
sql
create procedure p1()
begin
select * from emp;
end;
这个例子中,p1 是过程名,begin ... end 中间是要封装的 SQL 逻辑。创建完成后,通过下面的语句调用:
sql
call p1();
如果要查看过程定义,可以使用:
sql
show create procedure p1;
如果要删除过程,可以使用:
sql
drop procedure if exists p1;
在命令行里创建存储过程时,经常要先改结束符。因为过程体里面本身会出现分号,如果不改结束符,MySQL 可能在第一个分号处就认为语句结束了。
sql
delimiter $$
create procedure p1()
begin
select * from emp;
end $$
delimiter ;
这里的意思是:临时把整段 SQL 的结束符改成 $$,等存储过程创建完成后再改回分号。DataGrip 这类工具通常会帮我们处理一部分执行细节,但在命令行里这个点很容易踩坑。
二、变量:让过程内部能保存中间结果
存储过程里常见三类变量:系统变量、用户变量、局部变量。
系统变量是 MySQL 服务器提供的变量,可以分为会话级和全局级。会话级只影响当前连接,全局级通常影响后续新连接。
sql
show session variables;
show session variables like 'auto%';
show global variables like 'auto%';
select @@autocommit;
set session autocommit = 1;
用户变量由用户自己定义,作用范围是当前连接。它常用来接收输出参数,变量名前面带一个 @。
sql
set @myname := 'zhangsan';
set @myage := 20;
select @myname, @myage;
局部变量只在存储过程内部生效,需要先声明再使用。比如下面这个过程先声明一个计数变量,再把查询结果放进去:
sql
create procedure p2()
begin
declare stu_count int default 0;
select count(*) into stu_count from emp;
select stu_count;
end;
declare 声明了局部变量,select count(*) into stu_count 把查询结果赋值给变量,最后再查询这个变量。局部变量一般写在过程体开头,尤其是后面还要声明游标和 handler 时,声明顺序不能乱。
三、参数:输入、输出和原地修改
存储过程的参数分为三种:输入参数、输出参数、输入输出参数。
输入参数用于把外部数据传进过程。输出参数用于把过程内部结果带出去。输入输出参数既能传入,也能被过程修改后带出。
下面这个过程根据分数返回等级:
sql
create procedure check_score(in score int, out result varchar(10))
begin
if score >= 90 then
set result := '优秀';
elseif score >= 60 then
set result := '及格';
else
set result := '不及格';
end if;
end;
调用时,输入分数直接传值,输出结果用用户变量接收:
sql
call check_score(95, @result);
select @result;
如果一个参数既要传进去,又要在过程内部修改,可以使用输入输出参数:
sql
create procedure p5(inout score double)
begin
set score := score / 2;
end;
set @score := 180;
call p5(@score);
select @score;
这里的逻辑是把两百分制分数转换成百分制。调用前先把值放进用户变量,调用后再查看变量,就能看到过程修改后的结果。
四、流程控制:让过程具备判断和循环能力
只有参数和变量还不够,过程内部通常还需要判断和循环。
条件判断可以使用 if。前面的 check_score 就是一个典型例子:先判断是否大于等于 90,再判断是否大于等于 60,最后走不及格分支。
如果是多个固定区间或多个分支,也可以使用 case。比如根据月份判断季节:
sql
create procedure p6(in month int)
begin
declare season varchar(10);
case
when month >= 1 and month <= 3 then set season := '冬季';
when month >= 4 and month <= 6 then set season := '春季';
when month >= 7 and month <= 9 then set season := '夏季';
when month >= 10 and month <= 12 then set season := '秋季';
else set season := '输入错误';
end case;
select concat('您输入的月份是', month, '月,对应的季节是', season);
end;
循环常见三种写法:while、repeat、loop。
while 是先判断条件,再执行循环体:
sql
create procedure p7(in num int)
begin
declare sum int default 0;
while num >= 0 do
set sum := sum + num;
set num := num - 1;
end while;
select sum;
end;
repeat 是先执行一次,再判断是否退出:
sql
create procedure p8(in num int)
begin
declare sum int default 0;
repeat
set sum := sum + num;
set num := num - 1;
until num <= 0 end repeat;
select sum;
end;
loop 本身没有条件,需要配合 leave 或 iterate 控制流程。leave 类似 break,iterate 类似 continue。
sql
create procedure p10(in num int)
begin
declare sum int default 0;
myloop: loop
if num <= 0 then
leave myloop;
end if;
if num % 2 = 1 then
set num := num - 1;
iterate myloop;
end if;
set sum := sum + num;
set num := num - 1;
end loop myloop;
select sum;
end;
这个过程计算从 1 到 num 之间偶数的累加和。遇到奇数时先减 1,然后通过 iterate 直接进入下一轮;当 num 小于等于 0 时,通过 leave 退出循环。
五、游标和 handler:处理多行查询结果
普通变量适合保存单个值。如果查询结果是多行,就需要游标。游标可以把查询结果集一行一行取出来处理。
基本流程是:声明游标,打开游标,循环 fetch 数据,最后关闭游标。
sql
create procedure p11(in uage int)
begin
declare uname varchar(20);
declare ugender varchar(10);
declare u_cursor cursor for
select name, gender from emp where salary >= uage;
declare exit handler for not found close u_cursor;
create table if not exists emp_user_pro(
id int primary key auto_increment,
name varchar(20),
gender varchar(10)
);
open u_cursor;
while true do
fetch u_cursor into uname, ugender;
insert into emp_user_pro(name, gender) values(uname, ugender);
end while;
close u_cursor;
end;
这里要注意三个点。
第一,变量要先声明,游标再声明,handler 通常放在游标后面。MySQL 官方文档中的游标示例也是先声明普通变量,再声明 cursor,最后声明 handler。
第二,fetch 每次从游标取一行数据,放入 uname 和 ugender。取到数据后,再插入到 emp_user_pro 表中。
第三,游标取完后会触发 not found 条件。如果没有 handler,循环无法正常收尾。这里使用 exit handler,在取不到数据时关闭游标并退出当前过程块。
这个例子适合用来理解游标,但实际开发时也要谨慎:如果普通 insert into ... select ... 能解决问题,就不一定要写游标。游标更适合逐行处理逻辑明显不同的场景。
六、存储函数:带返回值的过程
存储函数和存储过程很像,但函数必须有返回值。它的参数只能作为输入参数使用,结果通过 return 返回。
下面的函数用于计算从 1 累加到 n 的结果:
sql
create function fun1(n int)
returns int deterministic
begin
declare total int default 0;
while n > 0 do
set total := total + n;
set n := n - 1;
end while;
return total;
end;
调用函数时,可以直接放在 select 中:
sql
select fun1(100);
这里的 returns int 表示函数返回 int 类型,deterministic 表示相同输入总是得到相同输出。笔记里提到的拼写也要注意:应该写 create function,不是 create fuction;应该写 returns 类型,而不是 return 类型。
七、常见错误和排查入口
第一类错误是结束符问题。命令行里创建过程时,如果没有使用 delimiter,过程体中的分号可能提前结束整条语句。遇到创建失败,可以先检查结束符。
第二类错误是声明顺序问题。局部变量、游标、handler 都要写在 begin 块内部,并且尽量放在可执行 SQL 前面。尤其是游标场景,变量、游标、handler 的顺序不要反过来。
第三类错误是输出参数接收方式不对。out 参数不能像普通返回值一样直接拿到,一般要用用户变量接收,再通过 select 查看。
第四类错误是游标没有处理取完数据的情况。fetch 到结果集末尾后会触发 not found,如果没有 handler,循环很容易异常结束。
第五类错误是函数语法写错。存储函数要写 create function、returns、return,并且函数参数只作为输入使用。
八、什么时候适合用存储过程
存储过程适合把数据库内部的重复 SQL 逻辑封装起来,尤其是逻辑主要围绕数据库表操作展开,并且希望减少应用层和数据库之间多次交互的时候。
但它不适合承载所有业务逻辑。如果规则经常变化,或者逻辑需要和接口、权限、缓存、消息队列等应用层能力配合,放在 Java 代码里通常更容易维护。
判断是否使用存储过程,可以抓住一句话:逻辑主要发生在数据库内部,并且需要复用、批量处理或减少网络往返,就可以考虑存储过程;如果逻辑变化频繁、依赖应用层上下文,就不要硬塞进数据库。
参考资料:MySQL 8.0 Reference Manual 中 create procedure/create function、cursors、declare handler 相关章节。