MySQL从5.0版本起便引入了存储过程与函数,这一特性在数据库发展进程中堪称一次重要突破。

简单来说,存储过程和函数将复杂的SQL逻辑打包为一个可复用的单元。这样一来,应用程序无需关心底层SQL细节,只需像调用普通函数或执行命令那样调用即可。
这里有一个基本区分:函数必须有返回值,而存储过程则没有。当然,这只是最直观的说法,后续将展开更多细节。
1. 存储过程概述
1.1 理解
含义:存储过程这个名称或许听起来有些复杂,但其核心思想很简单——就是将一组预先编译好的SQL语句打包在一起(英文名为Stored Procedure,直译为“存好的过程”)。
执行过程:存储过程预先存储在MySQL服务器上,当需要执行时,客户端只需发出调用命令,服务器便会依次运行事先存好的所有SQL语句。
好处:它为何值得关注?
- 简化操作:显著提升SQL语句的重用性,减轻开发人员的工作负担。
- 减少失误:代码只需编写一次,无需反复拼接SQL,出错概率自然降低。
- 减少网络传输:客户端无需将大量SQL语句通过网络发送给服务器,仅需传递一个调用命令。
- 提高安全性:业务逻辑封装在存储过程中,SQL语句不会暴露于网络,数据查询安全性更高。
与视图、函数的对比:
- 相同点:它们都能使开发更清晰、更安全,并减少网络传输量。这些优势均通过封装实现。
- 不同点:视图本质上是一个虚拟表,通常不直接操作底层数据表;而存储过程是程序化的SQL,可直接操作底层数据表,能够实现较为复杂的处理逻辑。
- 存储过程与函数:最直观的区别在于——函数必须有返回值,而存储过程没有。
1.2 分类
存储过程的参数类型有三种:IN、OUT和INOUT。据此可将其分为以下几类:
- 没有参数:无参数无返回
- 仅带IN类型:有参数无返回
- 仅带OUT类型:无参数有返回
- 同时带有IN和OUT:有参数有返回
- 带INOUT:有参数有返回(参数既作为输入也作为输出)
注意:IN、OUT、INOUT可以同时出现在一个存储过程中,并且可以包含多个参数。
2. 创建存储过程
2.1 语法分析
语法如下:
CREATE PROCEDURE 存储过程名(IN|OUT|INOUT 参数名 参数类型,...)
[characteristics ...]
BEGIN
存储过程体
END
# 调用
CALL 存储过程名();
编写存储过程时,有几个关键点需特别注意:
- 参数前的关键字:
IN:输入参数。存储过程只读取该参数的值。若未定义参数种类,默认即为IN。OUT:输出参数。存储过程执行完毕后,调用方可以获取参数中返回的值。INOUT:既是输入也是输出,传入时携带值,执行后可能发生变化。
- 形参类型可以是MySQL数据库中的任意数据类型。
characteristics是对存储过程的约束条件,主要包括:LANGUAGE SQL:表明存储过程体由SQL语句组成,当前系统仅支持SQL。[NOT] DETERMINISTIC:指明结果是否确定。若确定,相同输入将得到相同输出;若不确定,相同输入可能产生不同输出;默认值是不确定。{CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA}:说明子程序使用SQL语句的限制,默认值为CONTAINS SQL。SQL SECURITY {DEFINER | INVOKER}:执行权限。DEFINER表示仅创建者可执行,INVOKER表示具有访问权限的用户均可执行,默认值为DEFINER。COMMENT 'string':注释信息,用于描述存储过程。
- 存储过程体中可以包含多条SQL语句,但如果只有一条,可以省略BEGIN和END。
- 需要设置新的结束标记:MySQL默认以分号结束语句,但存储过程体中也包含分号,会导致冲突。因此需使用
DELIMITER修改结束符,编写完成后再恢复。例如:DELIMITER //将结束符改为//。- 存储过程定义完毕后,使用
DELIMITER ;恢复默认结束符。 - 注意:若使用Navicat等工具,工具会自动处理,无需手工设置。
2.2 代码举例
2.2.1 无参数(无参无返回)
存储过程体为普通查询语句,通过CALL调用即可。例如:
DELIMITER $
CREATE PROCEDURE select_all_data()
BEGIN
SELECT * FROM emps;
END $
DELIMITER ;
# 调用
CALL select_all_data();
2.2.2 带OUT(无参有返回)
SELECT的结果通过INTO赋值给变量。调用时传入@变量名,再使用SELECT查看变量值。例如:
DELIMITER //
CREATE PROCEDURE show_min_salary(OUT ms DOUBLE)
BEGIN
SELECT MIN(salary) INTO ms FROM emps;
END //
DELIMITER ;
# 调用
CALL show_min_salary(@ms);
SELECT @ms;
2.2.3 带IN(有参无返回)
传入一个参数作为筛选条件。调用时可直接传值,也可传入@变量名。例如:
DELIMITER //
CREATE PROCEDURE show_someone_salary(IN empname VARCHAR(20))
BEGIN
SELECT salary FROM emps WHERE ename = empname;
END //
DELIMITER ;
# 调用方式1
CALL show_someone_salary('Abel');
# 调用方式2
SET @empname = 'Abel';
CALL show_someone_salary(@empname);
2.2.4 带IN和OUT(有参有返回)
将前两种方式组合起来——输入员工姓名,输出员工薪资。例如:
DELIMITER //
CREATE PROCEDURE show_someone_salary2(IN empname VARCHAR(20), OUT empsalary DOUBLE)
BEGIN
SELECT salary INTO empsalary FROM emps WHERE ename = empname;
END //
DELIMITER ;
2.2.5 带INOUT(有参数且返回)
参数既作为输入也作为输出,执行后参数的值会被修改。例如:
DELIMITER //
CREATE PROCEDURE show_mgr_name(INOUT empname VARCHAR(20))
BEGIN
SELECT ename INTO empname FROM emps WHERE eid = (SELECT MID FROM emps WHERE ename=empname);
END //
DELIMITER ;
3. 调用存储过程
3.1 调用格式
存储过程必须通过CALL语句调用。若要调用其他数据库中的存储过程,需指定数据库名称,例如CALL dbname.procname。
不同参数模式的调用方式有所区别:
- in模式参数:
CALL sp1('值'); - out模式参数:
SET @name; CALL sp1(@name); SELECT @name; - inout模式参数:
SET @name=值; CALL sp1(@name); SELECT @name;
3.2 代码举例
以一个实用示例说明:实现累加运算,计算1+2+...+n的值。
DELIMITER //
CREATE PROCEDURE `add_num`(IN n INT)
BEGIN
DECLARE i INT;
DECLARE sum INT;
SET i = 1;
SET sum = 0;
WHILE i <= n DO
SET sum = sum + i;
SET i = i + 1;
END WHILE;
SELECT sum;
END //
DELIMITER ;
CALL add_num(50);
3.3 如何调试
MySQL的存储过程没有像Java或C++那样的专用集成调试环境。如何调试?最简单的办法是使用打印法——通过SELECT语句输出中间结果,验证SQL语句的正确性。调试成功后,将SELECT语句向后移动,继续调试下一段。也可以将存储过程中的SQL语句复制出来,逐段单独测试。
必须承认,调试过程确实相当繁琐。这意味着维护成本较高,因此在生产实践中,通常不推荐大量使用存储过程。
4. 存储函数的使用
MySQL支持自定义函数,定义完成后,调用方式与使用内置函数几乎相同。
4.1 语法分析
CREATE FUNCTION 函数名(参数名 参数类型,...)
RETURNS 返回值类型
[characteristics ...]
BEGIN
函数体 # 函数体中必须包含 RETURN 语句
END
说明:
- 函数中参数默认为IN,不能指定为OUT或INOUT。
RETURNS type必须指定返回类型,函数体里必须包含RETURN value语句。- characteristics与存储过程类似。
- 函数体也可以用BEGIN...END,当只有一条语句时可以省略。
4.2 调用存储函数
与调用系统函数相同:SELECT 函数名(实参列表)
4.3 代码举例
例如,创建一个函数查询某人的email:
DELIMITER //
CREATE FUNCTION email_by_name() RETURNS VARCHAR(25)
DETERMINISTIC CONTAINS SQL
BEGIN
RETURN (SELECT email FROM employees WHERE last_name = 'Abel');
END //
DELIMITER ;
SELECT email_by_name();
需要注意以下几点:
- 声明返回值类型时使用
RETURNS(带S),但函数体内的RETURN不带S。 - RETURN语句中不能出现分号。
- 函数参数前不需要写IN、OUT等关键字。
- 若创建时报错“you might want to use the less safe log_bin_trust_function_creators variable”,有两种处理方式:
- 添加必要的函数特性,例如
DETERMINISTIC和CONTAINS SQL。 - 执行
SET GLOBAL log_bin_trust_function_creators = 1;。
- 添加必要的函数特性,例如
4.4 对比存储函数和存储过程
| 关键字 | 调用语法 | 返回值 | 应用场景 | |
|---|---|---|---|---|
| 存储过程 | PROCEDURE | CALL 存储过程() | 理解为有0个或多个 | 一般用于更新 |
| 存储函数 | FUNCTION | SELECT 函数() | 只能是一个 | 一般用于查询结果为一个值并返回时 |
存储过程没有传统意义上的返回值,而是通过OUT参数“修改”变量的值。此外,存储函数可以放在查询语句中使用,而存储过程不可以。反过来,存储过程的功能更强大,能够执行创建表、删除表、事务操作等——这些是存储函数无法做到的。
5. 存储过程和函数的查看、修改、删除
5.1 查看
创建完成后,如何查看已经写好的存储过程?MySQL提供了几种方法:
- SHOW CREATE:查看创建信息,例如
SHOW CREATE FUNCTION test_db.CountProc \G - SHOW STATUS:查看状态信息,例如
SHOW PROCEDURE STATUS LIKE 'pattern',可以列出所有存储过程或函数的信息。 - information_schema.Routines表:直接查询系统表,获取最底层信息。
SELECT * FROM information_schema.Routines WHERE ROUTINE_NAME='存储过程或函数的名'。若存储过程和函数名称相同,需添加AND ROUTINE_TYPE = 'PROCEDURE'或'FUNCTION'来区分。
5.2 修改
修改存储过程或函数,并非更改功能或逻辑,而是修改相关特性。语法为:
ALTER {PROCEDURE | FUNCTION} 存储过程或函数的名 [characteristic ...]
可修改的特性包括:
CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATASQL SECURITY {DEFINER | INVOKER}COMMENT 'string'
例如:
ALTER PROCEDURE CountProc MODIFIES SQL DATA SQL SECURITY INVOKER ;
5.3 删除
删除存储过程或函数,使用DROP语句:
DROP {PROCEDURE | FUNCTION} [IF EXISTS] 存储过程或函数的名
添加IF EXISTS可以防止因对象不存在而报错。
6. 关于存储过程使用的争议
存储过程究竟是否应该使用?这个话题在业界一直存在争议。有些公司强制要求大型项目必须采用存储过程,而另一些公司则在开发手册中明确禁止。为何差异如此之大?
6.1 优点
- 一次编译多次使用:存储过程仅在创建时编译,后续调用无需再次编译,执行效率更高。
- 减少开发工作量:将代码封装为模块,复杂问题可拆解为小模块,模块之间能够复用,代码结构更清晰。
- 安全性强:可设置使用权限,类似于视图,能够控制特定用户才能执行。
- 减少网络传输量:每次使用只需调用存储过程,无需传输大量SQL语句。
- 良好的封装性:原本需要多次连接数据库才能完成的操作,现在一次调用即可完成。
6.2 缺点
尽管存在这些优点,但替代方案也众多。例如阿里巴巴的开发规范中明确写明:
【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
具体来说,存储过程的缺点包括:
- 可移植性差:MySQL、Oracle、SQL Server的存储过程不能通用,更换数据库就需要重写。
- 调试困难:仅少数DBMS支持存储过程调试。复杂的存储过程开发和维护都不容易。虽有第三方工具,但通常需要付费。
- 版本管理困难:数据表索引变更可能导致存储过程失效。软件版本迭代时,存储过程难以管理,因为缺乏版本控制。
- 不适合高并发场景:高并发情况下,数据库压力本身就是瓶颈。在进行分库分表、对可扩展性要求较高的场景中,存储过程反而会成为负担。
小结:存储过程是一把双刃剑——既便利,也存在局限。不同公司持不同态度,但无论如何,作为开发人员,掌握存储过程依旧是必备技能之一。毕竟,无论最终是否使用,理解它的工作原理以及能解决什么问题,都是基本功。
