本章介绍什么是存储过程,为什么要使用存储过程以及如何使用存储过程,并且介绍创建和使用存储过程的基本语法。
存储过程(Stored Procedure)是预先编译并存储在数据库中的一组 SQL 语句的集合,可以把它理解为数据库里的 “自定义函数 / 脚本”。与直接执行零散 SQL 语句不同,存储过程一旦创建,可被反复调用,无需重复编写相同的 SQL 逻辑。
下面以 “处理订单” 为例:
不用存储过程:你需要手动逐条执行 “查库存 → 锁库存 → 扣库存 → 通知客户 → 缺货/下单” 等多条 SQL,还要手动处理逻辑分支(有货 / 缺货)。
用存储过程:你把这整套逻辑(包括条件判断、多条 SQL、分支处理)写成一个存储过程(比如叫proc_handle_order),之后只要调用CALL proc_handle_order(订单ID),数据库就会自动执行整套流程。
下面是一个简单的存储过程:
-- 1. 临时修改语句结束符(避免存储过程内的;提前终止定义) DELIMITER // -- 2. 创建存储过程 -- 过程名:get_user_order_count -- 参数:IN user_id INT (输入参数:要查询的用户ID) -- OUT order_count INT (输出参数:返回该用户的订单总数) CREATE PROCEDURE get_user_order_count( IN user_id INT, OUT order_count INT ) BEGIN -- 查询指定用户的订单数量,并赋值给输出参数 SELECT COUNT(*) INTO order_count FROM `order` o WHERE o.user_id=user_id; END // -- 3. 恢复默认的语句结束符(;) DELIMITER ;
调用存储过程:
-- 1. 声明一个变量接收返回结果 SET @total_orders = 0; -- 2. 调用存储过程:查询用户ID=4的订单总数 CALL get_user_order_count(4, @total_orders); -- 3. 查看结果 SELECT @total_orders;
使用存储过程主要存在如下优点:
(1)提升执行效率:存储过程在首次创建时会被数据库引擎预编译,后续调用无需重新解析、编译 SQL 语句,直接执行编译后的执行计划,大幅减少了 SQL 执行的开销,尤其适合频繁执行的复杂逻辑(如多表关联查询、批量数据更新)。
(2)减少网络传输开销:复杂业务逻辑若通过应用程序执行,需要多次发送 SQL 语句到数据库服务器,而调用存储过程只需传递 “调用指令 + 参数”,大幅减少网络传输的数据量,尤其适合分布式系统。
存储过程虽然存在优点,但缺点也很明显,主要缺点如下:
(1)可维护性差:存储过程的代码 “藏” 在数据库内部,不像应用程序代码(如 Java/Python)那样有成熟的 IDE 支持。没有像编程语言那样的断点调试、日志追踪功能,排查错误只能靠打印临时变量或日志,效率极低。而且存储过程的修改记录无法像代码一样通过 Git 等工具追踪,多人协作时容易出现版本混乱、覆盖修改的问题。
(2)移植性差:存储过程是高度依赖数据库厂商的。MySQL 的存储过程语法和 Oracle、SQL Server 完全不兼容,如果业务需要从 MySQL 迁移到 Oracle,所有存储过程都要重写。
(3)开发成本高:业务逻辑分散在 “应用层代码” 和 “数据库存储过程” 两处,新人接手需要同时熟悉两套逻辑,团队协作成本高。并且,精通数据库 + 存储过程的开发人员比普通应用开发少,招聘和培养成本更高。
(4)安全性风险:虽然存储过程能降低 SQL 注入风险,但如果参数处理不当(比如拼接 SQL 字符串),依然可能被注入。还有,为了让应用调用存储过程,往往需要给应用账号赋予过高的数据库权限,一旦应用账号泄露,风险更大。
不同数据库(MySQL、SQL Server、Oracle)的存储过程语法略有差异,以下以 MySQL 为例,讲解存储过程的创建方法。
存储过程的语法如下:
DELIMITER // -- 修改语句结束符为 //(避免与存储过程内的 ; 冲突) CREATE PROCEDURE 存储过程名称([IN/OUT/INOUT 参数名 数据类型]) BEGIN -- 存储过程执行的SQL逻辑(可包含查询、插入、更新、删除、条件判断等) END // DELIMITER ; -- 恢复语句结束符为 ;
参数说明:
IN:输入参数(默认),仅用于向存储过程传递值,存储过程内修改不会影响外部;
OUT:输出参数,仅用于从存储过程返回值;
INOUT:输入输出参数,既可以传递值,也可以返回修改后的值。
示例1:无参数存储过程,仅仅查询用户表数据
-- 修改结束符 DELIMITER // -- 创建存储过程 CREATE PROCEDURE GetActiveUsers() BEGIN -- 查询status=1(未删除)的用户 SELECT user_id, name, gender, age FROM `user` WHERE gender IS NOT NULL; END // -- 恢复结束符 DELIMITER ; -- 调用存储过程 CALL GetActiveUsers();
示例2:带输入参数的存储过程,根据 ID 查询用户
-- 创建存储过程 > CREATE PROCEDURE GetUserById(IN id INT) -- 输入参数:用户ID BEGIN -- 校验参数合法性 IF id <= 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户ID必须为正整数'; END IF; -- 查询指定ID的用户 SELECT * FROM `user` WHERE user_id = id; END 0 row(s) modified. -- 查询用户ID为1的用户信息 > CALL GetUserById(1) user_id|name |gender|age|phone |email |address |create_time |update_time | -------+---------+------+---+-----------+--------------------+------------+-------------------+-------------------+ 1|张三-UPDATE|男 | 25|13800138000|zhangsan@example.com|北京市朝阳区建国路88号|2025-01-18 02:03:00|2026-01-05 07:01:50| 1 row(s) fetched. -- 查询用户ID为100的用户信息 > CALL GetUserById(100) user_id|name|gender|age|phone|email|address|create_time|update_time| -------+----+------+---+-----+-----+-------+-----------+-----------+ 0 row(s) fetched. -- 用户ID非法 > CALL GetUserById(-1) SQL 错误 [1644] [45000]: 用户ID必须为正整数
示例3:带输出参数的存储过程(统计用户总数)
-- 创建存储过程 > CREATE PROCEDURE GetUserCount(OUT total INT) -- 输出参数:用户总数 BEGIN SELECT COUNT(*) INTO total FROM `user`; -- 将统计结果赋值给输出参数 END 0 row(s) modified. -- 设置一个变量,用来存放用户总数 > SET @total=0 0 row(s) modified. -- 调用存储过程,获取用户数量 > CALL GetUserCount(@total) 1 row(s) modified. -- 查看结果 > SELECT @total @total| ------+ 12| 1 row(s) fetched.
创建完成后,通过CALL语句执行存储过程,语法根据参数类型略有不同。
(1)执行无参数存储过程
-- 执行示例1的存储过程 CALL GetActiveUsers();
执行后会直接返回user表中未删除用户的列表。
(2)执行带输入参数的存储过程
-- 执行示例2的存储过程,查询ID=10的用户 CALL GetUserById(10);
若传入非法参数(如CALL GetUserById(-5);),会触发自定义异常:用户ID必须为正整数。
(3)执行带输出参数的存储过程
需要先定义变量接收输出结果,再调用存储过程:
-- 定义变量接收输出结果 SET @total_users = 0; -- 执行示例3的存储过程,将结果赋值给@total_users CALL GetUserCount(@total_users); -- 查看结果 SELECT @total_users;
执行后SELECT语句会返回用户表的总记录数。