Appearance
什么是存储过程?
存储过程可以理解为:**保存在数据库里的“小程序 / 一段业务流程”**,由数据库在服务端执行,客户端只需要通过名字调用。
1. 存储过程的核心概念
1.1 一句话定义
存储过程(Stored Procedure)是在数据库中预先编译并保存的一段逻辑代码,它可以包含多条 SQL 语句和流程控制,由客户端通过 CALL 命令调用,由数据库服务器在内部执行。
- 它更接近“程序 / 函数”,而不仅仅是一条 SQL
- 代码存放在数据库端,而不是应用服务器
- 一次调用可以执行一系列复杂操作
1.2 和普通 SQL 的区别
| 对比项 | 普通 SQL 语句 | 存储过程(Stored Procedure) |
|---|---|---|
| 本质 | 单条或简单多条 SQL | 一段带流程控制的“数据库程序” |
| 存放位置 | 一般写在应用代码或脚本中 | 保存在数据库内部 |
| 调用方式 | 直接发送 SELECT/INSERT/UPDATE 等 | 用 CALL procedure_name(...) 触发执行 |
| 能力 | 声明式查询或更新 | 可以写条件分支、循环、异常处理、事务控制等 |
| 复用性 | 需要在每个应用中重复写 | 多个应用可以复用同一个存储过程 |
| 网络往返次数 | 多条 SQL 多次往返 | 由存储过程内部执行,客户端只发一次调用 |
1.3 PostgreSQL 中的 PROCEDURE vs FUNCTION
在 PostgreSQL 中,有两个容易混淆的概念:函数(FUNCTION) 和 存储过程(PROCEDURE):
| 项目 | FUNCTION | PROCEDURE |
|---|---|---|
| 定义关键字 | CREATE FUNCTION | CREATE PROCEDURE |
| 调用方式 | 可以在 SELECT 中调用 | 使用 CALL procedure_name(...) 调用 |
| 是否必须有返回值 | 通常有返回值 | 通常不强调返回值,主要是执行一段逻辑 |
| 典型使用场景 | 计算、转换、返回结果集 | 复杂业务流程、多步写库、事务控制 |
PostgreSQL 从 11 版本开始才引入 **`CREATE PROCEDURE` + `CALL`** 的语法;在此之前,大家习惯用 `FUNCTION` 来模拟“存储过程”。
2. 存储过程解决了什么问题?
2.1 减少网络往返
应用如果需要执行一系列操作,比如:
- 检查用户余额是否足够
- 扣减余额
- 记录一条交易明细
如果全部由应用层来控制:
- 要向数据库发送多次请求
- 在高延迟网络环境下,会放大 RT
而把这段逻辑写成存储过程后:
- 客户端只需要
CALL一次 - 数据库在内部完成多步操作
2.2 封装复杂业务逻辑
- 把一整套“业务流程”固化在数据库中
- 外部应用只需要关注“输入参数”和“调用成功 / 失败”
- 多个系统可以共用一套逻辑(例如:Web 后台、批处理程序都调用同一个存储过程来结算订单)
2.3 更细粒度的权限控制
- 可以给应用账号只开放“调用某存储过程”的权限
- 而不是直接开放底层表的
INSERT/UPDATE/DELETE权限 - 这样可以避免应用绕过业务规则直接操作底层数据
3. PostgreSQL 存储过程的基本语法
3.1 定义存储过程
在 PostgreSQL 中定义存储过程的基本形式:
sql
CREATE PROCEDURE procedure_name(arg_name arg_type, ...)
LANGUAGE plpgsql
AS $$
BEGIN
-- 在这里写多条 SQL 和流程控制
END;
$$;关键点:
LANGUAGE plpgsql:表示使用 PostgreSQL 的过程语言 PL/pgSQLBEGIN ... END;:过程体,里面可以写多条 SQL 和控制结构- 外层用
$$ ... $$包裹过程体字符串
3.2 调用存储过程
sql
CALL procedure_name(参数1, 参数2, ...);- 调用时只需传入参数,不需要重复写内部逻辑
4. 一个完整的业务场景示例:下单扣库存
下面通过一个**“下单扣库存”**的简单示例来理解存储过程的使用方式。
4.1 业务问题描述
假设我们有一个电商系统,业务要求:
- 用户下单时,需要检查库存是否足够
- 库存足够:
- 扣减商品库存
- 写入一条订单记录
- 库存不足:
- 不生成订单
- 返回错误信息
如果在应用层用多条 SQL 实现:
- 需要多次请求数据库
- 需要自己处理并发竞争(两个请求同时读到老库存)
- 手动管理事务,容易漏掉回滚逻辑
使用存储过程可以把以上逻辑打包在一次 `CALL` 调用里,由数据库内部负责事务控制与并发一致性。
4.2 准备原始数据表结构
sql
-- 商品表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
stock INTEGER NOT NULL DEFAULT 0 -- 当前库存
);
-- 订单表
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(product_id),
quantity INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);原始数据(假设表中已有如下记录)
sql
-- products 表
product_id | name | stock
-----------+------------+------
1 | iPhone 15 | 10
2 | iPad Pro | 5
-- orders 表(开始为空)
order_id | product_id | quantity | created_at
---------+------------+----------+------------
-- 暂无数据4.3 创建存储过程:下单扣库存
下面的存储过程:
- 输入:
p_product_id(商品 ID)、p_quantity(购买数量) - 行为:
- 检查库存是否足够
- 不足则抛出异常
- 足够则扣减库存并插入订单
sql
CREATE OR REPLACE PROCEDURE place_order(
IN p_product_id INTEGER,
IN p_quantity INTEGER
)
LANGUAGE plpgsql
AS $$
DECLARE
v_current_stock INTEGER; -- 当前库存
BEGIN
-- 1. 读取当前库存(并且 FOR UPDATE 锁定该行,防止并发修改)
SELECT stock INTO v_current_stock
FROM products
WHERE product_id = p_product_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION '产品不存在:id = %', p_product_id;
END IF;
-- 2. 检查库存是否足够
IF v_current_stock < p_quantity THEN
RAISE EXCEPTION '库存不足:当前库存 = %,请求数量 = %',
v_current_stock, p_quantity;
END IF;
-- 3. 扣减库存
UPDATE products
SET stock = stock - p_quantity
WHERE product_id = p_product_id;
-- 4. 插入订单
INSERT INTO orders(product_id, quantity)
VALUES (p_product_id, p_quantity);
-- 这里不需要显式 COMMIT,由外部事务或调用环境控制
END;
$$;这里使用了 `FOR UPDATE` 来锁定库存行,保证在并发情况下不会出现两个事务同时读到相同库存、重复扣减的问题。
4.4 分步展示执行过程
场景 1:库存足够的情况
我们调用存储过程,下单 3 台 product_id = 1 的商品。
sql
CALL place_order(1, 3);第 1 步:SELECT ... FOR UPDATE 读取库存
sql
-- 查询结果
product_id | name | stock
-----------+-----------+------
1 | iPhone 15 | 10 ← 读到当前库存 10,并加锁第 2 步:存储过程内部判断
v_current_stock = 10p_quantity = 3- 条件
10 < 3为假,不抛出异常
第 3 步:更新库存
sql
-- 执行后的 products 表
product_id | name | stock
-----------+------------+------
1 | iPhone 15 | 7 ← 库存从 10 变为 7
2 | iPad Pro | 5第 4 步:插入订单记录
sql
-- 执行后的 orders 表(示意)
order_id | product_id | quantity | created_at
---------+------------+----------+--------------------------
1 | 1 | 3 | 2025-11-16 10:00:00+08 ← 新增订单场景 2:库存不足的情况
如果此时再尝试下单 100 台 product_id = 1:
sql
CALL place_order(1, 100);
``>
**第 1 步:读取库存**
```sql
product_id | name | stock
-----------+-----------+------
1 | iPhone 15 | 7 ← 当前库存 7第 2 步:过程内部判断
v_current_stock = 7p_quantity = 100- 条件
7 < 100为真,触发:
text
RAISE EXCEPTION '库存不足:当前库存 = %,请求数量 = %', 7, 100;执行结果效果:
- 事务回滚(取决于外层事务设置)
products表的库存保持不变orders表不会插入新的订单记录
4.5 小结:这个例子体现了什么?
- 存储过程把一整套“下单扣库存”的流程封装了起来
- 应用层只需要关心:
CALL place_order(1, 3);成功 → 下单成功- 抛异常 → 失败原因(例如库存不足)
- 在高并发环境下,由于用了
SELECT ... FOR UPDATE:- 库存行有行锁,避免并发超卖
5. 适用场景、优缺点与注意事项
5.1 典型适用场景
- 多步写库 + 强事务一致性:
- 下单、结算、批量记账、资金划拨
- 对性能敏感的批处理逻辑:
- 大量数据处理时,减少网络往返
- 需要在数据库侧统一实现的规则:
- 审计、日志、数据清洗、数据同步
5.2 优点
- 减少网络往返:一次调用完成多步操作
- 集中业务规则:关键规则固化在数据库中,避免被绕过
- 更高复用性:不同应用 / 服务可以共享同一个存储过程
- 更强的数据一致性:结合事务、行锁等机制,直接在数据库层保证
5.3 潜在缺点
- 业务逻辑分散:一部分在应用,一部分在数据库,维护成本可能上升
- 版本管理困难:存储过程属于数据库对象,需要额外管理部署流程
- 调试相对麻烦:不像应用代码那样有丰富的调试工具
5.4 使用时的注意点
- 尽量让存储过程职责单一、清晰,避免写成“上千行的巨型脚本”
- 搭配一致的版本管理方案(例如:SQL 文件 + 版本号管理)
- 清晰区分:哪些逻辑放在应用层,哪些确实适合放在数据库层
6. 总结
- 存储过程本质上是保存在数据库中的小程序,可以封装多条 SQL 和流程控制,由数据库服务端执行。
- 在 PostgreSQL 中,可以使用
CREATE PROCEDURE定义,并通过CALL调用。 - 合理使用存储过程,可以:
- 减少网络往返
- 集中关键业务规则
- 提升数据一致性
- 但也需要注意:
- 控制复杂度
- 做好版本管理和运维
下一步你可以在本地 PostgreSQL 中,按照本示例亲手创建 `products` / `orders` 表和 `place_order` 存储过程,实际执行几次 `CALL`,用 `SELECT` 查看库存和订单的变化,会对“存储过程”有更直观的理解。