🐬 第23章 使用存储过程
📄 一句话概括

存储过程是「存在数据库里的一段程序」:能带参数、能写 IF 和循环、能被一句 CALL 调用。 这一章是全书最「编程」的一章。 教材那个例子是「算一张单的总额、可选择加税」: Demo 里同一个逻辑换参数调三次,20005 不加税 149.87、加税 158.86—— 和教材算出来的一致;本沙箱没有 CREATE PROCEDURE,用 Python 函数顶上。

📝 本章和前面几章的关系

上一章的视图只能封装一句 SELECT,这一章封装的是「一套动作」。 这一章开始进入「服务端编程」:存储过程、游标、触发器, 三者的共同点是「代码跑在数据库里,不在应用里」。

一、存储过程:把逻辑放进数据库

它就是一段有名字、有参数、存在库里的程序。调用方只要 CALL 名字(参数), 不必知道里面写了几条 SQL。 参数有三个方向:IN 传进去、OUT 带出来、INOUT 两头都用。 教材那个例子三个参数各用一次:单号 IN、是否加税 IN、总额 OUT。 它比视图强在「能写流程」:IF 判断、循环、临时变量、异常处理, 视图只能是一句 SELECT,一句里塞不下这些。

二、教材那个例子:ordertotal

目标:给一个单号,算出总额;第二个参数决定要不要加 6% 的税;结果通过 OUT 参数带出去。 MySQL 的写法(原码,本沙箱不执行): CREATE PROCEDURE ordertotal(IN onumber INT, IN taxable BOOLEAN, OUT ototal DECIMAL(8,2)), 过程体里 DECLARE 变量、SELECT ... INTO 赋值、IF taxable THEN 加税,最后把结果塞进 ototal。 调用方式很特别:CALL ordertotal(20005, 1, @total); 然后 SELECT @total;。 @total 是会话变量,用来接 OUT 参数——这也是 MySQL 独有的写法,别的库各不相同。 注意这句 SELECT ... INTO 变量: 它和普通 SELECT 的区别是结果不返回给客户端,而是存进变量。这是存储过程里取值的标准手段。

三、本沙箱怎么跑:Python 函数顶上

本沙箱没有 CREATE PROCEDURE(SQLite 压根不支持), 所以 Demo 用一个 Python 函数实现同样的逻辑:传连接、单号、是否加税,返回总额。 这不是「假装跑了」:SQL 真执行、数字真算出来, 只是「过程」这层壳换成了 Python 函数。 功能等价,语法不同——这正是本课一直在做的事。 20005 加税 158.86、20007 加税 1060.00,和教材那组数对得上(20005 的 149.87 乘 1.06)。

四、好处与代价

好处一:快。 过程在服务端预编译,省掉每次解析 SQL 的开销; 几十条语句一次调用只走一趟网络,而不是几十趟。 好处二:安全。 可以只给「执行这个过程」的权限,不给碰表的权限; 过程里还能加校验,把不规范的数据挡在门口。 好处三:复用。 业务规则只写一处,改规则改过程,不用满地找散落的 SQL。 代价一:不可移植。 MySQL、SQL Server、Oracle 各一套语法,写死了就绑死在这个库上。 代价二:难调试。 没有断点、没有日志,报错信息常常只给个行号。 代价三:版本管理麻烦。 过程存在库里,代码仓库看不到它,谁改了什么全靠人记。

五、小结

教材小结: 存储过程是存在库里的程序,带 IN/OUT 参数,能写流程;快、安全、可复用,但不可移植、难调试。 本课补三条:一是本沙箱没有存储过程,用函数顶上,逻辑等价; 二是 SELECT ... INTO 变量 是过程里取值的标准手段; 三是规则复杂又稳定才值得写,随时要改的别放进去。

六、动手 Demo:一个过程,三次调用

Demo 六段:先看这件事本身怎么算,再给 MySQL 的原码(不执行),然后用 Python 函数真跑三张单, 接着讲好处、讲代价,最后说清楚该不该用。 注意看 20005 那两个数:149.87 与 158.86,加税正好多 6%。

import sqlite3
con = sqlite3.connect(":memory:")

# 教材那个存储过程要做的事:算总额,可选择加 6% 的税
def ordertotal(conn, onumber, taxable):
    row = conn.execute("SELECT SUM(quantity * item_price) FROM orderitems "
                       "WHERE order_num = ?", (onumber,)).fetchone()
    total = row[0] or 0.0
    if taxable:
        total = total * 1.06
    return total

# 同一个逻辑换参数调用三次
for n in (20005, 20006, 20007):
    print(n, round(ordertotal(con, n, False), 2), round(ordertotal(con, n, True), 2))

# MySQL 侧的样子(原码,本沙箱不执行):
# CREATE PROCEDURE ordertotal(IN onumber INT, IN taxable BOOLEAN, OUT ototal DECIMAL(8,2))
# BEGIN ... SELECT SUM(quantity*item_price) INTO total ... END;
# CALL ordertotal(20005, 1, @total);

实跑输出:

--------------------------------------------------------------------
第二十三课:使用存储过程
--------------------------------------------------------------------

  【一】教材那个例子:算一张单的总额,还要能加税
  SQL> SELECT order_num,
       SUM(quantity * item_price) AS total FROM orderitems WHERE order_num = 20005
  order_num     total
  ------------------------
  20005         149.87
  (1 行)
  **这件事本身一句 SQL 就能干。** 但业务上还要求「能选择加不加 6% 的税」,
  **并且要能反复调用**——这就是存储过程要封装的东西。

  【二】MySQL 的写法(教材原码,本沙箱不执行)
  `CREATE PROCEDURE ordertotal(IN onumber INT, IN taxable BOOLEAN, OUT ototal DECIMAL(8,2))`
  `BEGIN`
  `  DECLARE total DECIMAL(8,2);`
  `  SELECT SUM(quantity * item_price) INTO total FROM orderitems WHERE order_num = onumber;`
  `  IF taxable THEN SELECT total * 1.06 INTO total; END IF;`
  `  SELECT total INTO ototal;`
  `END;`
  调用:`CALL ordertotal(20005, 1, @total);` 然后 `SELECT @total;`
  **三个参数方向**:IN 是传进去、OUT 是带出来、INOUT 两头都用。
  **本课的沙箱没有 CREATE PROCEDURE,这段只能看不能跑。**

  【三】本沙箱怎么真跑:用 Python 函数顶上
  单号 20005:不加税 149.87,加税 158.86
  单号 20006:不加税 55.00,加税 58.30
  单号 20007:不加税 1000.00,加税 1060.00
  **同一个逻辑被调用了三次,每次只换参数。** 这就是存储过程的价值:
  **把「怎么做」封起来,调用方只管「做什么」。**
  **注意 20005 那个数:149.87,加税 158.86**——和教材算出来的一致。

  【四】为什么用存储过程:三个实在的好处
  **一是快。** 存储过程在服务端预编译、直接执行,**省掉每次解析 SQL 和来回传网络的开销**;
  **一次调用能触发很多条语句,只走一趟网络。**
  **二是安全。** 可以只给人执行存储过程的权限,**不给它碰表的权限**,
  **而且过程里能加校验,挡住不规范的数据。**
  **三是复用。** 业务规则只写一处,**改规则改过程,不用满地找 SQL。**

  【五】代价:三个必须知道的坏处
  **一是不可移植。** MySQL、SQL Server、Oracle 的存储过程语法各一套,
  **写死了就绑死在这个库上**——这正是本课讲「换个引擎就得重写」的极端案例。
  **二是难调试。** 过程里没法像应用代码那样断点、打日志,**错了一行报错信息往往只给个行号。**
  **三是版本管理麻烦。** 过程存在数据库里,**代码仓库里看不到它**,
  **改了什么、谁改的,全靠人记。**

  【六】本课的位置:知道它存在、知道什么时候该用
  **教材的态度是「该用就用,别当银弹」**:**规则复杂、调用频繁、对性能敏感,用存储过程;**
  **规则简单、要换库、要版本管理,就留在应用层。**
  **本课后面几章的游标、触发器,都属于同一类「服务端编程」手段。**

第一段一句话算出 20005 的总额 149.87。 过程要封装的就是它。 第二段原码。 三个参数方向、DECLARE、SELECT INTO、IF,一眼能看出是「程序」。 第三段真跑。 三张单各算两次:不加税 149.87/55.00/1000.00,加税 158.86/58.30/1060.00。 第四段好处。 快、安全、复用,三条都是「减少往返与重复」。 第五段代价。 不可移植、难调试、版本管理麻烦,三条都是「维护成本」。 第六段结论。 复杂又稳定才值得写;要换库、要版本管理就留在应用层。

30 秒实验

  1. 把税率从 6% 改成 8%,20005 的加税总额会是多少?
  2. (先想再跑)如果第二参数传 NULL 而不是 0/1,这个函数会怎样?
  3. 用一条 SQL 算「所有单加税后的总额」,和逐单加总的结果对得上吗?

常见误区

  1. 以为存储过程能让所有查询变快。它省的是往返与解析,不是算得更快。
  2. 把业务规则全塞进存储过程。换库时这层最贵,而且版本管理很难。
  3. 以为 SQLite 也能写 CREATE PROCEDURE。它不支持,只能靠应用层或触发器。
自测三问(点开对答案) 1. 存储过程和视图的本质区别是什么? —— 视图只能装一句 SELECT;存储过程是一段程序,能带参数、写 IF 与循环、执行多条语句。 2. IN、OUT、INOUT 三种参数有什么区别? —— IN 传值进去,OUT 把结果带出来,INOUT 既可传入也可带出。 3. 存储过程最大的两个代价是什么? —— 不可移植(各库语法不同)和难维护(难调试、不进代码仓库、版本管理麻烦)。

存储过程能写流程了, 但 SQL 天生是「一次处理一批」。 要在过程里「一行一行地处理」,得请出游标——下一章。

相关笔记