🦏 第13章 存储程序
📄 一句话概括

存储程序是四样东西的总称: 存储过程(call 调)、存储函数(在 select 里用)、触发器(增删改时自动跑)、 事件(按时间自动跑)。 它们都是「把代码存进数据库」,好处是少一次往返、权限收口、多个应用共用一套逻辑。 本沙箱是 sqlite3,四种里只实现了触发器一种。 另外三种一律 syntax error, 所以这一章只有触发器是全程真跑的。

📝 本章和前面几章的关系

前面十二章都在跟「数据」打交道,从这一篇开始跟「代码」打交道——把逻辑也存进数据库里。 这是一个有争议的话题:有人觉得存储程序是数据库的核心能力,有人觉得它是技术债。 这一篇先把四种是什么讲清,代价放到最后说。

一、四样东西:过程、函数、触发器、事件

存储过程:用 call 手动调用, 可以带输入参数和输出参数,里面能写几十条语句。 它像数据库里的一个函数,只是得主动叫它。 存储函数:在 select 里当函数用, 比如 select 我的函数(身高) from students。 它必须返回一个值,而且不能改数据。 触发器: 不用调,增删改的时候自动跑。 「谁动了这张表,就顺手干这件事」—— 它的调用者不是人,是那条 insert。 事件:也不用调,按时间跑。 「每天凌晨三点清一次临时表」这种活归它。 四种的共同点是「代码存在库里、有名字、能被反复调用」。 区别只在两件事: 谁调用它、它能不能返回值。

二、它们和「在应用里写 SQL」有什么不一样

写在应用里:SQL 字符串拼在代码里,跟着代码进 git、跟着应用发布、跟着应用回滚。 写进数据库:SQL 存在库里, 改它不用发版,但也就不在 git 里了。 这是两者最本质的差别,后面「代价」那一段全从这儿来。 另一个差别是「谁在跑」:应用里的 SQL 在应用进程里跑,存储程序在数据库进程里跑。 所以它离数据更近、少一趟网络,也所以它更不好调试。

三、本沙箱只有触发器

把教材那四种建法逐条试一遍,结果很清楚: create procedure、create function、create event 三条全报 syntax error, 只有 create trigger 成功。 这不是写法不对,是这三样东西在这里压根不存在。 sqlite3 的设计取向是「库要小、要好嵌」—— 一个几百 KB 的库塞进手机、塞进浏览器,自然不能带一整套过程语言。 砍掉过程语言、留下触发器,是它的取舍。 所以这一章会明显不对称:13.4 触发器全程真跑, 13.2、13.3、13.5 只能拿 MySQL 原码讲原理,再给一个本沙箱做得到的等价物。 凡是标注「本沙箱不执行」的代码,都是这个原因,不是笔误。 等价物也不是凑数—— 它恰好能说明「这件事本来该由谁做」。

四、为什么要把代码存进数据库

第一是少一次往返。 一段要读五张表的逻辑写在应用里,就是五次查询、五趟网络; 写成存储过程,一次 call 就够。 局域网里差别不大,跨机房、跨可用区就差得明显。 第二是权限收口。 可以只给某个账号「执行这个存储过程」的权限,不给它表的写权限。 于是「谁能改成绩」由数据库保证,不靠应用自觉。 这一点视图也能做(第 11.1 篇),存储过程做得更彻底。 第三是多个应用共用一套逻辑。 Java 一份、Python 一份、报表工具一份,三份迟早不一致; 存进数据库就只有一份。 这和视图「把口径固定下来」是同一个思路。

五、存进去的代价

一是调试难。 应用代码能打断点、能看日志、能写单元测试; 存储过程只能靠 select 打印中间结果,或者写进临时表看。 出错了报错信息还常常只有一句「语法错在第几行」。 二是版本管理难。 代码在 git 里,存储过程在数据库里—— 改一次要两边同步。 回滚的时候最麻烦:代码回滚了,库里的过程还是新的, 两边的假设对不上,故障就来了。 三是可移植性差。 MySQL 的过程拿到 SQLite、PostgreSQL 都不能直接跑, 连 delimiter 这种写法都是 MySQL 客户端独有的。 本沙箱已经演示过:四样里三样直接语法错。 所以业界的默认倾向是「逻辑尽量放应用层」,存储程序留给「必须贴着数据做的事」: 触发器维护冗余字段、批量归档、定时清理。 凡是能在应用里干的,就别往数据库里塞。

六、动手 Demo:四种存储程序逐条建一遍,看哪几个能成

Demo 就一段:把四种建法放在一个循环里试,成功的打印「可以」,失败的把报错原样打出来。 这不是「演示」,是「探底」——先摸清手上这个引擎支持到哪儿,再决定后面几篇怎么写。

import _q46 as Q
c = Q.blank()
c.execute("CREATE TABLE students (id INTEGER PRIMARY KEY, "
          "name TEXT, height REAL)")

# 四种存储程序,逐条试一遍
cases = [
    ("存储过程", "CREATE PROCEDURE p1() BEGIN SELECT 1; END"),
    ("存储函数", "CREATE FUNCTION f1() RETURNS INT RETURN 1"),
    ("事件", "CREATE EVENT e1 ON SCHEDULE EVERY 1 DAY DO SELECT 1"),
    ("触发器", "CREATE TRIGGER tr1 AFTER INSERT ON students "
               "BEGIN UPDATE students SET height = 170 "
               "WHERE id = NEW.id; END"),
]
for name, sql in cases:
    try:
        c.execute(sql)
        print("%-6s 可以" % name)
    except Exception as e:
        print("%-6s 不行 -> %s" % (name, e))

实跑输出:

--------------------------------------------------------------------
第六十七课:存储程序——把代码存进数据库
--------------------------------------------------------------------

  存储程序是哪四样东西
  **「存储程序」是一类东西的总称**:**把 SQL 代码写进数据库、存下来、以后按名字调用。**
  **教材把它分成四种**:
  **存储过程、存储函数、触发器、事件。**
  **四种都是「存在库里的代码」,区别在「谁调用、什么时候跑」。**

  **存储过程**:**用 `call` 手动调**,可以带输入参数和输出参数,
  **里面能写几十条语句。** **像数据库里的一个「函数」。**
  **存储函数**:**在 `select` 里当函数用**,
  **`select 我的函数(列) from 表`**——**它必须返回一个值,不能改数据。**
  **触发器**:**不用调,增删改的时候自动跑。** **「谁动了这张表,就顺手干这件事」。**
  **事件**:**也不用调,按时间跑。** **「每天凌晨三点清一次临时表」。**

  这里能用的只有一样:触发器
  **本沙箱是 `sqlite3`,它只实现了四种里的一种。** **把教材那四种写法逐条试一遍就清楚了。**
  SQL> CREATE PROCEDURE p1()
  报错:near "PROCEDURE": syntax error
  SQL> CREATE FUNCTION f1() RETURNS INT
  报错:near "FUNCTION": syntax error
  SQL> CREATE EVENT e1 ON SCHEDULE EVERY 1 DAY
  报错:near "EVENT": syntax error
  **前三条全是 `syntax error`**——
  **不是写法不对,是这三样东西这里压根没有。**
  **第四条成功了,触发器是整个文件里唯一能跑的存储程序。**

  **所以这一章会很「不对称」**:**触发器全程真跑**;
  **存储过程、存储函数、事件只能拿 MySQL 原码讲原理,再给一个等价物。**
  **凡是标注「本沙箱不执行」的,都是这个原因,不是笔误。**

  为什么要把代码存进数据库
  **第一是少一次往返。** **一段要读五张表的逻辑,写在应用里就是五次查询、五趟网络**;
  **写成存储过程,一次 `call` 就够**——**在局域网里差别不大,跨机房就差得明显了。**
  **第二是权限收口。** **可以只给某个账号「执行这个存储过程」的权限,不给它表的写权限。**
  **于是「谁能改成绩」这件事由数据库保证,不靠应用自觉。**
  **第三是多个应用共用一套逻辑。** **Java 一份、Python 一份、报表工具一份,三份迟早不一致**;
  **存进数据库就只有一份。** **这和视图「把口径固定下来」是同一个思路。**

  存进去的代价
  **一是调试难。** **应用代码能打断点、能看日志、能单元测试**;
  **存储过程只能靠 `select` 打印中间结果,或者写进一张临时表看。**
  **二是版本管理难。** **代码在 `git` 里,存储过程在数据库里**——
  **改一次要两边同步,回滚的时候最麻烦:代码回滚了,库里的过程还是新的。**
  **三是可移植性差。** **MySQL 的过程拿到 SQLite、拿到 PostgreSQL 都不能直接跑**,
  **连 `delimiter` 这种写法都是 MySQL 独有的。**
  **这一条本沙箱已经演示过了——四样里三样直接语法错。**
  **所以业界的默认倾向是「逻辑尽量放应用层」**,**存储程序留给「必须贴着数据做的事」**:
  **触发器维护冗余字段、批量归档、定时清理。**

存储过程、存储函数、事件三条全是 syntax error,报错分别指向 PROCEDURE、FUNCTION、 EVENT 这三个词。 只有触发器那条执行成功。 所以整章的结构是:触发器真跑,另外三样讲原理加等价物。

30 秒实验

  1. 把 create trigger 那条也改成 create trigger tr1 after insert on nosuch_table,会报错吗? 为什么?
  2. 为什么「多应用共用一套逻辑」这件事,靠视图解决不了?
  3. 回滚代码却不回滚数据库里的存储过程,会造成什么现象?

常见误区

  1. 以为 syntax error 是自己写错了。 这三样在这里压根没有,换个引擎才有。
  2. 把业务逻辑大量塞进存储过程。 调试、版本管理、迁移三样全变难,能放应用层就放应用层。
  3. 以为存储程序能提升性能。 少的是网络往返,计算本身一点没少;跨机房才明显。
自测三问(点开对答案) 1. 四种存储程序,区别在哪两件事上? —— 谁调用它(手动 call、在 select 里用、增删改时自动、按时间自动)和它能不能返回值。 2. 本沙箱支持哪几种?为什么? —— 只有触发器。sqlite3 是嵌入式库,取向是「库要小、好嵌入」,砍掉了整套过程语言,只留触发器。 3. 存储程序最大的三个代价是什么? —— 调试难(没法打断点、报错信息少)、版本管理难(代码在 git、过程在库里,回滚不同步)、 可移植性差(换数据库就重写)。

知道有哪四样、也知道这里只剩一样了, 下一篇从最常见的那一样开始:13.2 存储过程——MySQL 里的那个 call。

相关笔记