过程是「收进名字里的一段 SQL」,函数是「能写进 SQL 的一个值」。
CREATE PROCEDURE、CALL、DELIMITER 这些词沙箱一个都不认,只能复刻语义。
DELIMITER 是客户端指令,不是 SQL 语法——它报语法错误,报得对。
14.1 是全书第一个「沙箱完全跑不了」的章节。教材讲建过程、建函数、变量、光标四件事。 这一篇的做法是:能复刻的用 Python 复刻并真跑(参数、返回值、光标、事务), 复刻不了的贴 MySQL 原码并标明不执行。 前面第 12 章讲视图,那是「把一句 SELECT 存起来」;这一篇更进一步,把一整段逻辑存进去。
一、五个关键字,沙箱一个都不认
先把话说清楚:SQLite 没有存储过程和存储函数。 五个关键字挨个试,全报语法错误,报的位置都在关键字上。
| 写法 | 沙箱的反应 |
|---|---|
CREATE PROCEDURE |
near "PROCEDURE": syntax error |
CREATE FUNCTION |
near "FUNCTION": syntax error |
CALL p1() |
near "CALL": syntax error |
DELIMITER $$ |
near "DELIMITER": syntax error |
BEGIN ... END |
near "SELECT": syntax error |
注意这不是「不支持某个特性」,而是根本不认识这些词。 所以从这一篇开始,教材里的过程代码一律走「贴原码 + 说明」的路子,不假装能跑。 能真跑的是「过程这个概念」:一段 SQL 收进一个名字,带参数、能被反复调用、 能返回一个值或一批行。 这些用 Python 一样能演出来,而且演出来的差别跟 MySQL 是一致的。
二、过程的本质:有名字的一段 SQL
写一个「加一件商品」的动作,包成有名字的东西,然后连调三次。
调用方只需要知道名字和参数,不用重复写那句 INSERT。
| 时机 | 行数 | total_changes |
|---|---|---|
| 调用之前 | 5 | — |
| 连调三次之后 | 8 | 8 |
行数从 5 变 8,total_changes 累加到 8。
注意这个数不是「这次插了几行」,是「这个连接一共改了几行」——这是 SQLite 的口径。
MySQL 里对应的是 ROW_COUNT():它给的是「上一条语句影响了几行」。
同名不同义这件事在本课出现过很多次,这里又是一处。
三、函数必须返回值,而且能进 SQL
把「按价格分档」写成一个函数,注册进连接之后就能直接写在 SQL 里。
SELECT name, price, goods_level(price) 这样跑,八行结果各带一个档位。
c.create_function(u"goods_level", 1, goods_level)
c.execute(u"SELECT name, price, goods_level(price) FROM tb_goods")
这是这一篇最像 MySQL 的一步:函数能出现在 SELECT 列表里,也能出现在 WHERE 里。
MySQL 的存储函数就是这种用法,写法一模一样。
| 对比项 | 过程 | 函数 |
|---|---|---|
| 有没有返回值 | 没有(可带回结果集) | 必须有 |
| 能不能写在 SQL 里 | 不能 | 能 |
| 怎么调 | CALL 名字(...) |
SELECT 名字(...) |
「过程不能写进 SQL」是硬限制,不是习惯问题。 因为过程可能改数据、可能返回多个结果集,而 SQL 表达式里只能放「一个值」。
四、三种参数模式
参数的「方向」有三类,MySQL 用三个关键字声明,沙箱只能靠调用约定表达。
| 模式 | 意思 | Python 里的写法 |
|---|---|---|
IN |
只进不出(默认) | 普通参数 |
OUT |
带一个结果出来 | 传个容器进去 |
INOUT |
进去、改完、再出来 | 同一个容器 |
实测三组:IN 传 100.0 拿回 113.0;OUT 传空容器拿回 100;
INOUT 传 [70] 拿回 140。
三个数互不相同,正好说明「方向」确实不一样。
最容易记错的是 OUT:调用方要准备好接,不然结果就丢了。
MySQL 那边写 CALL p(1, @total),调完还要 SELECT @total 才看得到。
五、变量:三种作用域
教材在这一节讲了三种变量,分清它们比记语法重要。
| 种类 | 写法 | 活多久 |
|---|---|---|
| 用户变量 | @name |
整个连接 |
| 局部变量 | DECLARE name |
过程体那一对 BEGIN ... END |
| 参数 | IN name INT |
调用期间 |
第一种带 @,是「连接级」的,换个连接就没了;第二种只能写在过程体开头,
而且必须写在所有语句之前;第三种是调用时传进来的。
常见的错法是「把 DECLARE 写在语句中间」——MySQL 要求它必须在开头一段,
顺序错了报的不是「位置不对」,而是语法错误,很难看出原因。
六、光标:一行一行地处理
光标就是把结果集摊开,一行一行地拿。
沙箱里就是 fetchone() 循环,取完给 None,循环结束。
| 商品 | 库存 |
|---|---|
| 螺丝刀 | 200 |
| 电钻 | 30 |
| 手套 | 500 |
| 安全帽 | 60 |
| 交换机 | 8 |
| 卷尺 | 100 |
| 口罩 | 900 |
| 网钳 | 40 |
八行逐行累加,合计 1838。
这里用光标是「为了演光标」——真要求合计,一句 SUM(stock) 就够了,而且快得多。
光标的正当用途是「每行都要做点别的事」:逐行调接口、逐行算复杂规则、逐行写日志。
MySQL 里要写四步:DECLARE ... CURSOR、OPEN、FETCH、CLOSE,
还要加一个 CONTINUE HANDLER FOR NOT FOUND,不然取到最后一行会报错退出。
七、DELIMITER 为什么必须换
教材里每一段过程代码的第一行都是 DELIMITER $$,很多人直接照抄不知道为什么。
DELIMITER $$
CREATE PROCEDURE p1()
BEGIN
SELECT 1;
SELECT 2;
END$$
原因很实际:过程体里本来就有分号,而客户端默认拿分号当「这条语句结束」。
不换结束符,客户端读到第一个分号就把半截语句发出去,必然报语法错。
DELIMITER $$ 的意思是「从现在起,看到 $$ 才算一条语句结束」。
它属于 mysql 命令行客户端,不是 SQL 语法——用程序调接口时根本不需要写它。
八、动手 Demo:没有存储过程,就把语义复刻出来
这一段先让五个关键字各报一次错,把「沙箱没有」这件事定下来;
然后用 Python 复刻过程、函数、三种参数模式、光标,每一段都真跑。
最后贴 MySQL 的游标与 DELIMITER 原码。
import sqlite3
c = sqlite3.connect(u":memory:")
# ① 五个关键字,一个都不认
for sql in [u"CREATE PROCEDURE p1() BEGIN SELECT 1; END",
u"CREATE FUNCTION f1(x INT) RETURNS INT RETURN x",
u"CALL p1()", u"DELIMITER $$"]:
try:
c.execute(sql)
except sqlite3.Error as e:
print(e)
# ② 过程:有名字的一段 SQL
def add_goods(name, cat_id, price, stock):
c.execute(u"INSERT INTO tb_goods (name, cat_id, price, stock) "
u"VALUES (?, ?, ?, ?)", (name, cat_id, price, stock))
return c.total_changes
print(c.execute(u"SELECT COUNT(*) FROM tb_goods").fetchone()[0])
for nm, cid, pr, st in [(u"卷尺", 1, 15.0, 100), (u"口罩", 2, 2.0, 900)]:
print(add_goods(nm, cid, pr, st))
print(c.execute(u"SELECT COUNT(*) FROM tb_goods").fetchone()[0])
# ③ 函数:注册进 SQL,就能出现在 SELECT 里
def goods_level(price):
return u"高价" if price >= 300 else (u"中价" if price >= 50 else u"低价")
c.create_function(u"goods_level", 1, goods_level)
print(c.execute(u"SELECT name, goods_level(price) FROM tb_goods").fetchall())
# ④ 光标:一行一行拿
cur = c.execute(u"SELECT name, stock FROM tb_goods ORDER BY id")
total = 0
while True:
row = cur.fetchone()
if row is None:
break
total += row[1]
print(total)
实跑输出:
--------------------------------------------------------------------
第六十课:写过程之前,先承认沙箱没有这个过程
--------------------------------------------------------------------
**教材 14.1 讲四件事:建过程、建函数、变量、光标。**
**沙箱这四个关键字一个都不认,所以这一课先看清「不认」长什么样。**
**看清之后,用 Python 把同样的语义复刻一遍——过程是「有名字的一段 SQL」,**
**这个概念跟语言无关,换个宿主照样能演。**
一、五个关键字,沙箱一个都不认
--------------------------------------------------------------------
写法 沙箱的反应
----------------------------------------
建过程 near "PROCEDURE": syntax error
建函数 near "FUNCTION": syntax error
调过程 near "CALL": syntax error
换结束符 near "DELIMITER": syntax error
语句块 near "SELECT": syntax error
**五行全是语法错误,报的位置都在关键字上:`near "PROCEDURE"`、`near "CALL"`。**
**这说明它不是「不支持某个特性」,而是根本不认识这些词。**
**所以从这一课开始,凡教材里的过程代码一律走「贴原码 + 说明」的路子,不假装能跑。**
二、过程的本质:有名字的一段 SQL
--------------------------------------------------------------------
时机 行数 `total_changes`
-----------------------------------
调用之前 5 —
连调三次之后 8 8
**三次调用,行数从 5 变 8,`total_changes` 一路累加到 8。**
**注意最后那个数不是「这次插了几行」,是「这个连接一共改了几行」——这是 SQLite 的口径。**
**过程的好处就在这儿:调用方只需要知道「名字 + 参数」,不用重复写那句 `INSERT`。**
**它是把 SQL 收进一个名字里,跟它跑在数据库里还是跑在应用里没关系。**
三、函数必须返回值,而且能进 SQL
--------------------------------------------------------------------
mysql> SELECT name, price, goods_level(price) AS lv FROM tb_goods
name price lv
-----------------
螺丝刀 25 低价
电钻 380 高价
手套 8 低价
安全帽 120 中价
交换机 680 高价
卷尺 15 低价
口罩 2 低价
网钳 45 低价
8 rows in set
**这是这一课最像 MySQL 的一步:Python 函数注册进连接之后,就能写在 SQL 里。**
**MySQL 的存储函数也是这样用的——`SELECT` 列表里能写,`WHERE` 里也能写。**
对比项 过程 函数
-------------------------------------------------------------
有没有返回值 没有(可以带回结果集) 必须有
能不能写在 SQL 里 不能 能
怎么调 `CALL 名字(...)` `SELECT 名字(...)`
四、三种参数模式
--------------------------------------------------------------------
模式 这次传的 Python 里的写法 拿到的
----------------------------------------
IN `100.0` 普通参数 113.0
OUT 空容器 函数往里写 100
INOUT `[70]` 同一个容器进出 140
**`IN` 是最常见的:只进不出,算完给个结果就完事。**
**`OUT` 和 `INOUT` 在 Python 里没有专门语法,只能靠返回值或者传个容器进去。**
**MySQL 那边是三个关键字写在参数前面,`OUT total INT` 这样。**
**沙箱没有参数声明这一层,所以「模式」这件事只能靠调用约定表达。**
五、光标:一行一行地处理
--------------------------------------------------------------------
商品 库存
------------
螺丝刀 200
电钻 30
手套 500
安全帽 60
交换机 8
卷尺 100
口罩 900
网钳 40
**这就是光标:`fetchone()` 一次取一行,取完给 `None`,循环结束。**
**MySQL 里要写 `DECLARE 游标 CURSOR FOR SELECT ...`、`OPEN`、`FETCH`、`CLOSE` 四步,**
**还要声明一个 `CONTINUE HANDLER` 处理「取不到数据了」这个事件。**
**合计库存 1838。**
**注意这里是「逐行累加」,不是 `SUM()`——光标的用处就是「每行都要做点别的事」,**
**只求和的话一句 `SUM` 就够了,别用光标。**
[教材原码 · 本沙箱不执行]
| DECLARE cur CURSOR FOR SELECT name, stock FROM tb_goods;
| DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
| OPEN cur;
| read_loop: LOOP
| FETCH cur INTO v_name, v_stock;
| IF done THEN LEAVE read_loop; END IF;
| SET total = total + v_stock;
| END LOOP;
| CLOSE cur;
教材原码,MySQL 的游标要四步 + 一个异常处理器
六、DELIMITER 为什么必须换
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| DELIMITER $$
| CREATE PROCEDURE p1()
| BEGIN
| SELECT 1;
| SELECT 2;
| END$$
教材原码,第一条永远是 DELIMITER,不是 CREATE
**原因很实际:过程体里本来就有分号,而客户端默认拿分号当「这条语句结束」。**
**不换结束符,客户端读到第一个分号就把半截语句发给服务器,必然报语法错。**
**`DELIMITER $$` 的意思是「从现在起,看到 `$$` 才算一条语句结束」。**
**它是 `mysql` 命令行客户端的功能,不是 SQL 语法——所以写进程序里调接口是不需要的。**
**这一条最容易糊涂:`DELIMITER` 不是 SQL 语句,是客户端的指令。**
**沙箱把它报成语法错误,报得对——它本来就不该出现在 SQL 里。**
第一组五行报错都落在关键字上,near "PROCEDURE"、near "CALL"、near "DELIMITER"。
这说明它连词法都不认,不是「功能缺失」。
第二组三个数:调用前 5 行,三次调用后 8 行,total_changes 累加到 8。
注意它一直在累加,不会清零——这是「这个连接一共改了几行」,不是「这条语句影响几行」。
第三组八行是函数进了 SELECT 的结果,每行带一个档位。
这一步和 MySQL 的存储函数用法完全一致。
第四组最后一个数是库存合计。
它由逐行累加得到,跟一句 SUM 的结果一样,但过程多跑了七次循环——
这就是「光标只在必须逐行处理时才用」的原因。
30 秒实验
- 把注册函数的参数个数从 1 改成 2,再用一个参数的写法调它,看报什么错。
- 把光标那段改成
SELECT SUM(stock),比对结果是不是同一个数。 - 把
add_goods的INSERT故意写错一个列名,看报错落在哪一层。
常见误区
- 以为
DELIMITER是 SQL 语句,写进程序里调接口,结果报语法错。 - 以为
ROW_COUNT()和 SQLite 的total_changes是一回事,一个是「这次」,一个是「累计」。 - 求个合计也上光标,多写二十行代码、还慢了一个数量级。
自测三问(点开对答案)
1. 过程和函数最本质的区别是什么? —— 函数必须返回一个值,而且能写进 SQL(`SELECT` 列表、`WHERE` 都能放); 过程没有返回值,只能 `CALL`,但可以带回一个甚至多个结果集。 2. `DELIMITER` 是 SQL 语句吗? —— 不是,它是 `mysql` 命令行客户端的指令。 作用是改「语句结束符」,好让过程体里的分号不被当成语句结尾。用程序调接口时不需要它。 3. 什么情况下才该用光标? —— 每一行都要做不同的事(调接口、写日志、复杂逐行计算)时才用。 只是求和、求平均这类,一句聚合函数就够了,光标慢得多。过程建好了,下一篇讲怎么调:CALL 与 SELECT 的分工,以及三种参数方向怎么传。
- ☐ (本篇完,下一篇 14.2 调用存储过程和存储函数)