从0到1搞懂 SQL 存储过程:原理、实战与避坑指南

从0到1搞懂 SQL 存储过程:原理、实战与避坑指南

这是一篇为小白打造的由浅入深教程:读完你将知道什么是存储过程、为什么/何时使用、如何快速上手(MySQL 与 PostgreSQL 示例),以及常见坑与工程化最佳实践。


一、什么是存储过程(Stored Procedure)?

  • 存储过程是预先保存在数据库中的一段 SQL 语句集合,可以带参数,支持变量、流程控制、异常处理。
  • 本质:把“多条 SQL + 逻辑”封装成一个可复用的数据库端函数/过程,通过一次调用完成复杂操作。
  • 价值:减少网络往返、封装业务数据逻辑、提升批量处理性能、统一安全与审计。

对比:

  • 视图:封装查询,不含复杂流程控制;存储过程:可包含增删改查、循环、条件与事务控制。
  • 函数(不同引擎差异):多用于返回值计算;过程更偏向执行动作(PG11+ 也有真正的 PROCEDURE)。

二、何时该用存储过程?

  • 需要在数据库内进行批处理、报表汇总、数据清洗。
  • 高频、稳定的复合操作(例如下单写多表、结算日批量跑账)。
  • 对外隐藏表结构与复杂逻辑,只暴露调用接口。
  • 对延迟敏感,减少多次往返请求。

不建议过度使用的场景:

  • 快速变化的业务规则,大量逻辑更适合应用层管理与版本控制。
  • 强依赖多数据源/外部服务的流程,维护成本高。

三、快速上手:MySQL 实战

下面以 MySQL 为例,展示常用语法与模式。

1)创建最简存储过程

注意 MySQL 中需要修改分隔符避免与语句内的分号冲突。

DELIMITER //
CREATE PROCEDURE sp_get_user_by_id(IN p_user_id INT)
BEGIN
  SELECT id, name, email
  FROM users
  WHERE id = p_user_id;
END //
DELIMITER ;

-- 调用
CALL sp_get_user_by_id(1001);

2)IN / OUT / INOUT 参数

DELIMITER //
CREATE PROCEDURE sp_count_orders_by_user(
  IN p_user_id INT,
  OUT p_order_count INT
)
BEGIN
  SELECT COUNT(*) INTO p_order_count
  FROM orders
  WHERE user_id = p_user_id;
END //
DELIMITER ;

-- 调用并取出 OUT 参数
SET @cnt = 0;
CALL sp_count_orders_by_user(1001, @cnt);
SELECT @cnt AS order_count;

3)变量与流程控制(IF、CASE、WHILE)

DELIMITER //
CREATE PROCEDURE sp_apply_discount(IN p_user_id INT, IN p_rate DECIMAL(5,2))
BEGIN
  DECLARE v_level VARCHAR(10);
  DECLARE v_factor DECIMAL(5,2) DEFAULT 1.00;

  SELECT level INTO v_level FROM members WHERE user_id = p_user_id;

  IF v_level = 'gold' THEN
    SET v_factor = 0.80; -- 8 折
  ELSEIF v_level = 'silver' THEN
    SET v_factor = 0.90; -- 9 折
  ELSE
    SET v_factor = 1.00;
  END IF;

  -- 将折扣应用到该用户未支付订单
  UPDATE orders
  SET amount = amount * v_factor * (1 - p_rate)
  WHERE user_id = p_user_id AND status = 'pending';
END //
DELIMITER ;

4)事务与异常处理(HANDLER)

DELIMITER //
CREATE PROCEDURE sp_transfer(
  IN p_from INT,
  IN p_to   INT,
  IN p_amount DECIMAL(10,2)
)
BEGIN
  DECLARE exit HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK;
  END;

  START TRANSACTION;

  UPDATE accounts SET balance = balance - p_amount WHERE id = p_from;
  UPDATE accounts SET balance = balance + p_amount WHERE id = p_to;

  COMMIT;
END //
DELIMITER ;

要点:

  • DECLARE ... HANDLER 捕获异常,出错即回滚。
  • 注意 DDL 可能隐式提交;在过程内尽量避免混用 DDL 与 DML 事务。

5)返回结果集 vs 返回 OUT 参数

  • 直接 SELECT ...:客户端收到结果集(更适合查询类过程)。
  • OUT 参数:适合返回单值或少量聚合指标。

四、PostgreSQL 实战(PL/pgSQL)

PostgreSQL 11 之前常用 FUNCTION 承担“过程”角色;PG11+ 引入 PROCEDURE + CALL,且 PROCEDURE 能进行顶级事务控制。

1)等价函数(返回结果集)

CREATE OR REPLACE FUNCTION fn_get_user_by_id(p_user_id INT)
RETURNS TABLE(id INT, name TEXT, email TEXT)
LANGUAGE plpgsql AS $$
BEGIN
  RETURN QUERY
  SELECT u.id, u.name, u.email
  FROM users u
  WHERE u.id = p_user_id;
END;
$$;

-- 调用
SELECT * FROM fn_get_user_by_id(1001);

2)PROCEDURE 与事务、异常

CREATE OR REPLACE PROCEDURE sp_transfer(p_from INT, p_to INT, p_amount NUMERIC)
LANGUAGE plpgsql AS $$
BEGIN
  BEGIN
    PERFORM 1; -- 占位
    UPDATE accounts SET balance = balance - p_amount WHERE id = p_from;
    UPDATE accounts SET balance = balance + p_amount WHERE id = p_to;
  EXCEPTION WHEN OTHERS THEN
    RAISE NOTICE 'transfer failed: %', SQLERRM;
    ROLLBACK; -- 仅在 CALL 的事务块层面有效
    RETURN;
  END;
END;
$$;

-- 调用
CALL sp_transfer(1, 2, 100.00);

提示:在 PG 中,FUNCTION 内不能 COMMIT/ROLLBACKPROCEDURE 通过 CALL 可进行更灵活的事务控制(需在合适的事务上下文中)。


五、典型应用场景示例

1)日终汇总报表

  • 输入:日期区间
  • 过程:
    • 批量扫描订单表,聚合出 GMV、客单价、各品类销量
    • 将结果写入 daily_metrics 表(可幂等:先删后插或 UPSERT)
-- MySQL 示例(幂等写入)
DELIMITER //
CREATE PROCEDURE sp_build_daily_metrics(IN p_date DATE)
BEGIN
  REPLACE INTO daily_metrics(stat_date, gmv, orders, avg_order)
  SELECT p_date,
         SUM(amount) AS gmv,
         COUNT(*)    AS orders,
         AVG(amount) AS avg_order
  FROM orders
  WHERE DATE(paid_at) = p_date AND status = 'paid';
END //
DELIMITER ;

2)审计日志写入(统一入口)

DELIMITER //
CREATE PROCEDURE sp_write_audit(
  IN p_actor INT,
  IN p_action VARCHAR(64),
  IN p_target VARCHAR(64),
  IN p_payload JSON
)
BEGIN
  INSERT INTO audit_logs(actor_id, action, target, payload, created_at)
  VALUES(p_actor, p_action, p_target, p_payload, NOW());
END //
DELIMITER ;

六、优缺点速览

优点:

  • 降低网络往返、批量处理性能好
  • 封装复杂逻辑、隐藏表结构细节
  • 权限控制更集中(只授予 EXECUTE

缺点:

  • 版本化与测试相对应用层更困难
  • 跨数据库差异大,迁移成本高
  • 过度依赖会造成“数据库里堆业务”,降低可维护性

七、工程化与最佳实践

  • 版本管理:
    • 使用迁移工具(如 Flyway、Liquibase、dbmate)管理 CREATE/ALTER/DROP PROCEDURE,确保可重复部署。
    • 设计幂等:CREATE OR REPLACE(PG)、DROP ... IF EXISTS + CREATE(MySQL 8+ 部分对象支持)。
  • 命名与分层:
    • 前缀化按域名空间:如 sp_order_*sp_user_*
    • 一个过程只做一件事,输入输出清晰。
  • 性能:
    • 优先保证 SQL 可用索引;避免非 SARGable 条件(函数包裹列、隐式类型转换)。
    • 批处理分批提交,控制事务粒度,避免超长事务。
  • 安全:
    • 仅授予 EXECUTE 权限;敏感表不直接对应用账号开放。
    • 动态 SQL 需参数化(PG 用 format() + quote_ident/quote_literal,MySQL 用预编译)。
  • 测试:
    • 准备固定测试数据集,写回归用例;比对结果集与副作用表。
    • 为关键过程建立慢日志监控与执行计划审查。

八、常见坑与避坑技巧

  • MySQL 分隔符 DELIMITER 忘改,导致创建失败。
  • 在过程内混入 DDL 触发隐式提交,打断事务一致性。
  • 未捕获异常导致半更新;应设置 HANDLER(MySQL)或 EXCEPTION(PG)。
  • 大事务长时间持锁,引发阻塞或死锁;应细分批次与合理索引。
  • 过度返回大结果集,丧失“减少往返”的价值;返回必要字段。
  • 跨引擎移植:语法、数据类型、异常与事务行为差异大。

九、小抄(Cheat Sheet)

  • MySQL 创建:DELIMITER // ... CREATE PROCEDURE ... END // DELIMITER ;
  • PG 函数:CREATE OR REPLACE FUNCTION ... LANGUAGE plpgsql,返回 TABLE 可直接 SELECT * FROM fn(...)
  • PG 过程:CREATE OR REPLACE PROCEDURE ...; CALL sp(...);
  • 参数类型:IN / OUT / INOUT
  • 异常:MySQL DECLARE ... HANDLER;PG EXCEPTION WHEN ... THEN ...
  • 事务:在过程内谨慎控制,避免 DDL;PG 需 PROCEDURE 配合 CALL 进行顶级事务控制。

十、如何继续学习?

  • 选一台本地数据库(MySQL 或 PostgreSQL),搭建测试库与样例表。
  • 照抄上面的示例,逐条运行理解每个关键字的效果。
  • 给真实需求写一个“日终任务”过程:输入日期、输出统计,做成幂等反复执行。
  • 引入迁移工具,把存储过程纳入 CI/CD 与版本管理。

彩蛋:梗版总结(轻松读法)

  • 如果把数据库比作餐馆,存储过程就是“后厨套餐”:把“洗菜-切菜-爆炒-装盘”一口气端上来,少跑堂(少网络往返),还不容易漏盐(统一校验与事务)。
  • IN/OUT 参数像“外卖备注”和“小票回执”,查一嘴、带点料,结果还能装兜里带走。
  • DECLARE ... HANDLER 就是“后厨灭火器”:锅里着火别慌,先灭火(ROLLBACK)再说;
  • DELIMITER 像“临时改快捷键”:不改的话,厨师喊一声“;”服务员就以为上菜结束了。
  • 事务就是“购物车结算”:全选一起付(COMMIT),有一个不对劲就全撤(ROLLBACK)。
  • 少整“超级无敌大锅菜”长时间炖(超长事务),邻桌都等饿了(锁阻塞)。
  • 记得做“可复制的招牌菜”(迁移脚本 + 幂等创建),下次翻台一键复刻。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值