Skip to content

什么是存储过程?

存储过程可以理解为:**保存在数据库里的“小程序 / 一段业务流程”**,由数据库在服务端执行,客户端只需要通过名字调用。

1. 存储过程的核心概念

1.1 一句话定义

存储过程(Stored Procedure)是在数据库中预先编译并保存的一段逻辑代码,它可以包含多条 SQL 语句和流程控制,由客户端通过 CALL 命令调用,由数据库服务器在内部执行。

  • 它更接近“程序 / 函数”,而不仅仅是一条 SQL
  • 代码存放在数据库端,而不是应用服务器
  • 一次调用可以执行一系列复杂操作

1.2 和普通 SQL 的区别

对比项普通 SQL 语句存储过程(Stored Procedure)
本质单条或简单多条 SQL一段带流程控制的“数据库程序”
存放位置一般写在应用代码或脚本中保存在数据库内部
调用方式直接发送 SELECT/INSERT/UPDATECALL procedure_name(...) 触发执行
能力声明式查询或更新可以写条件分支、循环、异常处理、事务控制等
复用性需要在每个应用中重复写多个应用可以复用同一个存储过程
网络往返次数多条 SQL 多次往返由存储过程内部执行,客户端只发一次调用

1.3 PostgreSQL 中的 PROCEDURE vs FUNCTION

在 PostgreSQL 中,有两个容易混淆的概念:函数(FUNCTION)存储过程(PROCEDURE)

项目FUNCTIONPROCEDURE
定义关键字CREATE FUNCTIONCREATE PROCEDURE
调用方式可以在 SELECT 中调用使用 CALL procedure_name(...) 调用
是否必须有返回值通常有返回值通常不强调返回值,主要是执行一段逻辑
典型使用场景计算、转换、返回结果集复杂业务流程、多步写库、事务控制

PostgreSQL 从 11 版本开始才引入 **`CREATE PROCEDURE` + `CALL`** 的语法;在此之前,大家习惯用 `FUNCTION` 来模拟“存储过程”。

2. 存储过程解决了什么问题?

2.1 减少网络往返

应用如果需要执行一系列操作,比如:

  1. 检查用户余额是否足够
  2. 扣减余额
  3. 记录一条交易明细

如果全部由应用层来控制:

  • 要向数据库发送多次请求
  • 在高延迟网络环境下,会放大 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/pgSQL
  • BEGIN ... END;:过程体,里面可以写多条 SQL 和控制结构
  • 外层用 $$ ... $$ 包裹过程体字符串

3.2 调用存储过程

sql
CALL procedure_name(参数1, 参数2, ...);
  • 调用时只需传入参数,不需要重复写内部逻辑

4. 一个完整的业务场景示例:下单扣库存

下面通过一个**“下单扣库存”**的简单示例来理解存储过程的使用方式。

4.1 业务问题描述

假设我们有一个电商系统,业务要求:

  1. 用户下单时,需要检查库存是否足够
  2. 库存足够:
    • 扣减商品库存
    • 写入一条订单记录
  3. 库存不足:
    • 不生成订单
    • 返回错误信息

如果在应用层用多条 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 = 10
  • p_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 = 7
  • p_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` 查看库存和订单的变化,会对“存储过程”有更直观的理解。