CALL 过程(...) 调过程,SELECT 函数(...) 调函数,两者不能互换。
函数能进 WHERE,所以能参与筛选;过程不能,只能把条件写进自己的 SQL 里。
过程里的几条语句是一伙的:要么全成、要么全不成,事务边界得自己划。
14.2 讲调用,教材只有两小节:调过程、调函数。这一篇补上「为什么函数能进 SQL、过程不能」。 上一篇讲怎么建,这一篇讲怎么用。 下一篇讲怎么看——三篇合起来才是过程的一生。
一、CALL 与 SELECT 的分工
| 对象 | 怎么调 | 能进 SQL 吗 |
|---|---|---|
| 存储过程 | CALL p(...) |
不能 |
| 存储函数 | SELECT f(...) |
能 |
「不能」是硬限制:SELECT 后面、WHERE 后面都放不进一个过程。
因为过程可能改数据、可能返回多个结果集,而 SQL 表达式里只能放「一个值」。
反过来想就通了:能在表达式里出现的东西,必须「输入一组值、输出一个值」。
过程不满足这个形状,所以进不去。
二、过程:按名字调,可以带回一批数据
调 p_list_cat(1),拿回两行:螺丝刀、电钻。
| id | name | price |
|---|---|---|
| 1 | 螺丝刀 | 25.0 |
| 2 | 电钻 | 380.0 |
过程可以返回结果集,而且可以返回不止一个——教材里那种「先查总数、再查明细」的写法。
这是它和函数最大的区别:函数的输出是一个值,过程的输出可以是一张表。
MySQL 客户端会把过程里的每个 SELECT 都当成一个结果集依次显示。
用程序调的时候要小心:fetchall() 只拿到第一个结果集,后面的要另外取。
三、函数:能进 SQL,所以能进 WHERE
把 goods_level(price) 放进 WHERE,直接筛出「高价」的两行。
这一句换成 MySQL 的存储函数,写法一模一样。
| 写法 | 结果 |
|---|---|
WHERE goods_level(price) = '高价' |
电钻、交换机 |
WHERE price >= 300 |
同样两行 |
两种写法的结果是同一个,但代价不同:函数版要在每一行上算一次。
八行看不出差别,八百万行时这个开销就是实打实的。
所以有一条经验:函数里别放重活,能在 WHERE 里直接写的条件就别包成函数。
它还会让索引失效——第 10 章那条「函数包在列上,索引就废了」在这一样成立。
四、三种参数方向,调用写法不一样
| 模式 | 调用写法 | 拿到的 |
|---|---|---|
IN |
f(100.0) |
113.0 |
OUT |
f(空容器) |
函数往里写 |
INOUT |
f([50]) |
100 |
IN 只进不出;OUT 得先准备一个容器接结果;INOUT 是同一个容器进去、改完出来。
Python 里这三种靠「返回值、容器、容器」凑出来,MySQL 里是三个关键字声明的。
MySQL 的调用写法要特别记一下:OUT 参数传的是用户变量,CALL p(1, @total),
调完还要再 SELECT @total 才拿得到值。这一步很容易漏,漏了就以为过程没算出结果。
五、事务边界:过程里的语句是一伙的
插三条:第一条、第二条正常,第三条主键撞了。 结果行数从 5 开始,回滚之后还是 5——前两条一起没了。
| 时机 | 行数 |
|---|---|
| 整段执行之前 | 5 |
| 回滚之后 | 5 |
这就是「事务边界」:过程里的几条语句要么全成、要么全不成。
但要注意——这个边界是你划的,不是过程自带的。
MySQL 里过程体不会自动开事务,START TRANSACTION 和 COMMIT 得自己写。
「过程自带事务」是一个很常见的误解,它的后果是:中途失败时前面几条已经落库了。
六、动手 Demo:CALL、SELECT,和三种参数方向
这一段把「调用」拆成四件事跑:过程带回结果集、函数进 WHERE、三种参数方向各调一次、
最后用一段故意失败的事务看边界在哪。
import sqlite3
c = sqlite3.connect(u":memory:")
# ① 过程:按名字调,带回一批行
def list_by_cat(cat_id):
return c.execute(u"SELECT id, name, price FROM tb_goods "
u"WHERE cat_id = ?", (cat_id,)).fetchall()
print(list_by_cat(1))
# ② 函数:注册进 SQL,就能进 WHERE
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 FROM tb_goods "
u"WHERE goods_level(price) = '高价'").fetchall())
# ③ 三种参数方向
def in_only(price): # IN:只进不出
return round(price * 1.13, 2)
def out_only(out): # OUT:结果写进调用方给的容器
out.append(300 // 3)
return out[-1]
def inout(box): # INOUT:同一个容器进出
box[0] = box[0] * 2
return box[0]
print(in_only(100.0), out_only([]), inout([50]))
# ④ 事务边界:第三条撞主键,前两条一起回滚
c.execute(u"BEGIN")
try:
c.execute(u"INSERT INTO tb_goods (name) VALUES ('甲')")
c.execute(u"INSERT INTO tb_goods (name) VALUES ('乙')")
c.execute(u"INSERT INTO tb_goods (id, name) VALUES (1, '撞了')")
c.execute(u"COMMIT")
except sqlite3.IntegrityError as e:
c.execute(u"ROLLBACK")
print(e)
print(c.execute(u"SELECT COUNT(*) FROM tb_goods").fetchone()[0])
实跑输出:
--------------------------------------------------------------------
第六十一课:怎么调——CALL、SELECT,和三种参数方向
--------------------------------------------------------------------
**教材 14.2 讲调用:`CALL` 调过程,`SELECT` 调函数。**
**沙箱两个都没有,但「按名字调用」这件事用 Python 一样能演,而且能演出差别。**
**这一课的重点是那个差别:函数能进 SQL,过程不能。**
一、CALL 与 SELECT 的分工
--------------------------------------------------------------------
对象 怎么调 能出现在 SQL 里吗
--------------------------------------------
存储过程 `CALL p(...)` 不能
存储函数 `SELECT f(...)` 能
**「不能」这一条是硬限制:`SELECT` 后面、`WHERE` 后面都放不进一个过程。**
**因为过程可能改数据、可能返回多个结果集,而 SQL 表达式里只能放「一个值」。**
二、过程:按名字调,可以带回一批数据
--------------------------------------------------------------------
id name price
-----------------
1 螺丝刀 25.0
2 电钻 380.0
**过程可以返回结果集,而且可以返回不止一个(教材里那种「先查总数再查明细」的写法)。**
**这是它和函数最大的区别:函数的返回值只有一个值,过程的输出可以是一张表。**
三、函数:能进 SQL,所以能进 WHERE
--------------------------------------------------------------------
mysql> SELECT name FROM tb_goods WHERE goods_level(price) = '高价'
name
------
电钻
交换机
2 rows in set
**上面这句在 MySQL 里就是把 `goods_level(price)` 换成一个存储函数,写法一模一样。**
**函数能进 `WHERE`,所以它能参与筛选;过程不能,只能自己把条件写进 SQL 里。**
**代价是函数会在每一行上被调一次。**
**几行看不出什么,五百万行时这个开销就明显了——所以函数里别放重活。**
四、三种参数方向,调用的写法不一样
--------------------------------------------------------------------
模式 调用写法 拿到的
---------------------------------
`IN` `f(100.0)` 113.0
`OUT` `f(容器)` 算出来的结果
`INOUT` `f(box)` 100
**`IN` 只进不出;`OUT` 得先准备一个容器接结果;`INOUT` 是同一个容器进去、改完出来。**
**Python 里这三种靠「返回值、容器、容器」凑出来,MySQL 里是三个关键字声明的。**
**最容易记错的是 `OUT`:调用方要准备好接,不然结果就丢了。**
**MySQL 那边写 `CALL p(1, @total)`,调完再 `SELECT @total` 才拿得到。**
五、事务边界:过程里的几条语句是一伙的
--------------------------------------------------------------------
**第三条插不进去,报的是 `UNIQUE constraint failed: tb_goods.id`。**
时机 行数
------------------
整段执行之前 5
回滚之后 5
**前两条插入被一起回滚了,行数一个没变。**
**这就是「事务边界」:过程里的几条语句要么全成、要么全不成。**
**MySQL 里这个过程体是包在 `START TRANSACTION` 和 `COMMIT` 之间的,**
**而过程本身不会替你开事务——这一点常被误解成「过程自带事务」。**
第一组两行是过程带回的结果集,cat_id = 1 的两件商品。
过程能带回一张表,函数只能带回一个值——差别就在这。
第二组两行验证了「函数能进 WHERE」,这和直接写 price >= 300 结果一样。
一样的结果、不同的代价:函数版每行都要算一次。
第三组三个数 113.0、100、100——第三个是把 50 翻倍得来的。
三个数各走一条路,正好对应三种参数方向。
第四组最后那个数是 5,和插之前一样:前两条被一起回滚了。
报错原文是 UNIQUE constraint failed: tb_goods.id,说明是第三条撞的。
30 秒实验
- 把
out_only([])改成out_only(None),看会报什么错,想想为什么。 - 把函数那段改成
WHERE price >= 300,比对结果和开销。 - 把第三条
INSERT换成一条语法错误的语句,看IntegrityError还抓不抓得住。
常见误区
- 把过程写进
WHERE,报语法错之后以为是自己参数写错了。 - 以为过程自带事务,中途失败时前面几条已经落库还不知道。
- 调完
OUT参数忘了再查一次用户变量,以为过程没算出结果。
自测三问(点开对答案)
1. 为什么过程不能写进 `WHERE`? —— 因为 SQL 表达式里只能放「输入一组值、输出一个值」的东西。 过程可能改数据、可能返回多个结果集,形状不对,所以放不进去。 2. `OUT` 参数在调用时最容易犯什么错? —— 忘了传一个能接结果的容器(MySQL 里是用户变量),或者调完忘了 `SELECT @变量` 取值。 结果不是没算出来,是没人接住。 3. 过程会自动开事务吗? —— 不会。事务边界得自己在过程体里写 `START TRANSACTION` 和 `COMMIT`。 以为它自带事务,中途失败时前面几条就已经落库了。能建能调了,下一篇讲「怎么看」:过程存在哪、源码怎么查、参数怎么翻。
- ☐ (本篇完,下一篇 14.3 查看存储过程和存储函数)