存储程序是四样东西的总称:
存储过程(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 秒实验
- 把
create trigger那条也改成create trigger tr1 after insert on nosuch_table,会报错吗? 为什么? - 为什么「多应用共用一套逻辑」这件事,靠视图解决不了?
- 回滚代码却不回滚数据库里的存储过程,会造成什么现象?
常见误区
- 以为
syntax error是自己写错了。 这三样在这里压根没有,换个引擎才有。 - 把业务逻辑大量塞进存储过程。 调试、版本管理、迁移三样全变难,能放应用层就放应用层。
- 以为存储程序能提升性能。 少的是网络往返,计算本身一点没少;跨机房才明显。
自测三问(点开对答案)
1. 四种存储程序,区别在哪两件事上? —— 谁调用它(手动 call、在 select 里用、增删改时自动、按时间自动)和它能不能返回值。 2. 本沙箱支持哪几种?为什么? —— 只有触发器。sqlite3 是嵌入式库,取向是「库要小、好嵌入」,砍掉了整套过程语言,只留触发器。 3. 存储程序最大的三个代价是什么? —— 调试难(没法打断点、报错信息少)、版本管理难(代码在 git、过程在库里,回滚不同步)、 可移植性差(换数据库就重写)。知道有哪四样、也知道这里只剩一样了, 下一篇从最常见的那一样开始:13.2 存储过程——MySQL 里的那个 call。
- ☐ (本篇完,下一篇 13.2 存储过程)