SQL 更多知识:使用存储过程

本章介绍什么是存储过程,为什么要使用存储过程以及如何使用存储过程,并且介绍创建和使用存储过程的基本语法。

 

什么是存储过程

存储过程(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语句会返回用户表的总记录数。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号