CREATE PROCEDURE 名字(参数) BEGIN … END,
用 CALL 名字(参数) 调。 它是「存在库里的、能带参数的一段 SQL」。
DELIMITER $$ 那几行是给客户端看的,不是给数据库看的——
因为过程体里的分号会被客户端当成语句结束。
本沙箱没有存储过程。 等价物是宿主语言的函数:
参数就是 IN、返回值就是 OUT、executescript 就是过程体。
上一篇把四样东西过了一遍,这一篇细讲最常用的那样:存储过程。
它是四样里唯一「人能主动叫」的——
触发器、事件都是自动跑的,函数只能嵌在 select 里。
所以要讲「怎么把一段逻辑封装起来反复调」,就得讲它。
一、MySQL 里长什么样
create procedure 名字(in 参数 类型, out 参数 类型) begin … end,
调用是 call 名字(值, @变量)。
外面那几行 delimiter $$ 和 delimiter ; 最容易让人懵。 它们不是 SQL 语句,是客户端命令
——告诉客户端「接下来到 $$ 才算一条语句结束」。
因为过程体里本来就有分号,客户端一见到分号就以为语句完了、急着发出去,
于是还没到 end 就被截断。 这不是数据库的毛病,是「怎么判断一条语句结束」这个老问题。
在应用代码里反而不用 delimiter:整段字符串是一次性送给驱动的,
驱动知道「这是一个 create procedure」,会自己找到结尾。
只有命令行和 Navicat 这类交互式客户端才需要它。
二、三个方向的参数:IN、OUT、INOUT
in 是输入:调用时把值传进去,过程里改它不影响外面。 这是默认方向,不写就是 in。
out 是输出:
过程里给它赋值,调用完外面能读到。 用 @变量 接——
call add_student('张伟', 175.5, @new_id); select @new_id;。
inout 是两头:
传进去一个值,过程里改,改完再拿出来。
用得最少,但「传一个计数器进去、加一、拿出来」这种场景只能用它。
这三个方向是 MySQL 自己发明的,别的数据库叫法不一样(SQL Server 用 output,
Oracle 用 in out)。 所以要跨库的代码,最好别依赖这些细节。
三、过程体里能写什么
能写普通的 select/insert/update/delete,也能写流程控制:
if … then … else … end if、while … do … end while、case、loop。
能声明局部变量:declare 变量名 类型 default 初值;。
注意 declare 必须写在 begin 之后、其他语句之前,
写后面会报错——这一条和很多语言「变量随处声明」的习惯不一样。
还能写异常处理:declare … handler for sqlexception begin … end,
相当于 try … catch。 这是 MySQL 里唯一能「抓住错误继续跑」的地方,
普通 SQL 语句一错就整条回滚。
注意「不能做的事」:存储过程里不能建表、不能改表结构(动态 SQL 除外,
要走 prepare/execute)。 所以「用存储过程做数据库迁移」这个想法是不通的。
四、本沙箱没有,等价物是「宿主语言的函数」
create procedure 在这里报 syntax error,
delimiter、declare、set @变量、call 也全是语法错。 一个都不认。
但「存储过程」这件事的本质很简单:
一段能带参数、能被反复调用的 SQL 逻辑。 这东西在 Python 里就是一个函数。
于是对应关系很直接:in 参数 = 函数的普通参数,out 参数 = 函数的返回值,
begin … end 里的多条语句 = 函数体里的多条 execute。 call = 调那个函数。
差在哪? 差在「在哪跑」:存储过程在数据库进程里跑,一段逻辑一趟网络都不到;
Python 函数里每 execute 一次就是一趟。 局域网里可以忽略,跨机房要算一算。
五、批量:一次送一整段
存储过程里常写几十条语句,这边的对应物是 executescript:
一次送一整段,中间的分号不会被当成结束。
这就是 Python 版的 delimiter——
execute 一次只认一条语句(送两条会报 You can only execute one statement at a time),
executescript 才认一整段。
一个容易忽略的差别:executescript 会先隐式 commit 一次,
再执行整段。 所以它和事务配合时要小心,别指望把它塞进一个 begin 里当成原子操作。
六、查看、修改、删除(MySQL 侧)
查看:show procedure status 列所有过程,show create procedure 名字 看它的定义,
information_schema.routines 是原始表。注意 show procedure status 需要 execute 权限,
没权限就一行都看不到——这常被误认为「过程不存在」。
修改:
MySQL 没有 alter procedure 改过程体这条语句。 要改就得 drop 再 create。
和第 11.3 篇「视图没有 alter view」一模一样。 alter procedure 只能改「特性」——
比如 sql security、注释,改不了逻辑。
删除:drop procedure if exists 名字;。
if exists 建议都带——迁移脚本要能重复跑。
本沙箱这边「查看」就是看代码文件、「修改」就是改函数、「删除」就是删函数。
连 if exists 都不用写,因为这些压根不在数据库里。
这也是「逻辑放应用层」的一个隐性好处:工具链是现成的。
七、动手 Demo:用 Python 函数当存储过程,再试四个不认的词
Demo 四段:写一个函数当「存储过程」并调用、用它插一行取回新 id、executescript 一次送四条语句、
最后逐条试 MySQL 那四个词。
第一段和第三段的对照是关键:
execute 只认一条,executescript 认一整段——这就是 Python 这边对 delimiter 的回答。
import sqlite3
import _q46 as Q
c = Q.con()
# 一个「存储过程」:校验 + 插入 + 交出新的 id
def add_student(conn, name, height):
cur = conn.cursor()
cur.execute("INSERT INTO students (name, height) VALUES (?, ?)",
(name, height))
conn.commit()
return cur.lastrowid
new_id = add_student(c, "张伟", 175.5) # 相当于 CALL + OUT 参数
print("新 id =", new_id)
Q.show(c, "SELECT id, name, height FROM students")
# 一次送一整段,分号不会被当成结束
c.executescript("""
CREATE TABLE a (x INT);
CREATE TABLE b (y INT);
INSERT INTO a VALUES (1), (2), (3);
INSERT INTO b SELECT x * 10 FROM a;
""")
Q.show(c, "SELECT * FROM b")
# MySQL 过程语言里的这几个词,这里一个都不认
for s in ("DELIMITER $$", "DECLARE x INT", "SET @a = 1", "CALL p1()"):
try:
c.execute(s)
except sqlite3.Error as e:
print(s, "->", e)
实跑输出:
--------------------------------------------------------------------
第六十八课:存储过程——MySQL 里的那个 call
--------------------------------------------------------------------
MySQL 里长什么样
MySQL 原码(本沙箱不执行):
| DELIMITER $$
| CREATE PROCEDURE add_student(
| IN p_name VARCHAR(20),
| IN p_height DECIMAL(4,1),
| OUT p_id INT
| )
| BEGIN
| INSERT INTO students (name, height) VALUES (p_name, p_height);
| SET p_id = LAST_INSERT_ID();
| END $$
| DELIMITER ;
**调用:`CALL add_student('张伟', 175.5, @new_id); SELECT @new_id;`**
**`delimiter $$` 这几行是给客户端看的,不是给数据库看的。** **过程体里本来就有分号**,
**客户端一见到分号就以为语句结束了**,**所以要先把「语句结束符」临时改成 `$$`。**
**这是 MySQL 命令行和 Navicat 这类客户端的老问题**,
**在应用代码里调 API 时反而不需要 `delimiter`**——**因为整段字符串是一次性送过去的。**
这里没有 CREATE PROCEDURE
SQL> CREATE PROCEDURE add_student(IN p_name TEXT)
报错:near "PROCEDURE": syntax error
SQL> DELIMITER $$
报错:near "DELIMITER": syntax error
SQL> DECLARE x INT
报错:near "DECLARE": syntax error
SQL> SET @a = 1
报错:near "SET": syntax error
SQL> CALL p1()
报错:near "CALL": syntax error
**四条全报 `syntax error`**:`procedure`、`delimiter`、`declare`、`set @变量`、
`call`——**这些词这里一个都不认。**
**`sqlite3` 的设计取向是「库要小、要好嵌」**,**所以把「过程语言」这一整层砍掉了**,只留触发器。
**这不是能力不足,是取舍。**
等价做法:用宿主语言写「过程」
**存储过程本质上就是「一段能带参数、能被反复调用的 SQL 逻辑」。**
**这东西在 Python 里就是一个函数。**
**`add_student(c, '张伟', 175.5)` 一句调用,内部插入一行并返回新 id。**
**参数就是形参、返回值就是 OUT 参数。**
**跑一次看看:**
返回的 id 是 1。
SQL> SELECT id, name, height FROM students
id name height
-----------------------
1 张伟 175.50
(1 行)
**这就是「存储过程」的全部作用**:
**把「插入 + 取新 id + 提交」包成一个动作。** **区别只是它在应用进程里,不在数据库进程里。**
批量:executescript 一次送多条
**存储过程里常写几十条语句**,**这边的对应物是 `executescript`**:
**一次送一整段,中间的分号不会被当成结束。**
SQL> SELECT * FROM b
y
---
10
20
30
(3 行)
**注意 `(1), (2), (3)` 这种多行插入是 SQLite 从 3.7.11 起才有的**,
**MySQL 上一直可以这么写。** **`insert into b select …` 两边都支持。**
IN、OUT、INOUT 怎么模拟
**`in` 参数**就是函数的普通参数,**`out` 参数**就是返回值。
**一个返回值不够用,就返回元组或者字典。**
**`inout` 就是「传进去、改完再拿出来」**,
**Python 里用一个可变对象(列表、字典)或者返回新值都行。**
**这一层语言本身就有,不需要数据库提供。**
**反过来说,这也是「把逻辑放应用层」更省事的地方**:
**参数怎么传、怎么返回,编程语言早就解决了**,
**数据库的过程语言还得自己发明一套 `in`/`out`/`inout`。**
查看、修改、删除(MySQL 侧)
MySQL 原码(本沙箱不执行):
| SHOW PROCEDURE STATUS WHERE Db = 'mydb';
| SELECT routine_name, created FROM information_schema.routines
| WHERE routine_type = 'PROCEDURE';
| SHOW CREATE PROCEDURE add_student;
| DROP PROCEDURE IF EXISTS add_student;
**MySQL 没有「修改存储过程」这条语句**:
**要改就得 `drop` 再 `create`**——
**和第 11.3 篇「视图没有 `alter view`,只能先删再建」一模一样。**
**本沙箱这边「查看」就是看代码文件、「修改」就是改函数、「删除」就是删函数**,
**连 `if exists` 都不用写**——**因为这些东西压根不在数据库里。**
第一段调用返回 1,插入成功,select 能看到张伟 175.5。
第二段 executescript 建了两张表、插了三行、把 a 的数据乘 10 灌进 b,
b 里是 10/20/30。
第三段四个词全报 syntax error,分别指向 DELIMITER、DECLARE、SET、CALL。
30 秒实验
execute送两条语句会报什么?executescript为什么不报?- 把
add_student改成「先查重、有就更新、没有才插入」,它对应存储过程里的哪个写法? - 为什么
declare必须写在begin之后、其他语句之前?
常见误区
- 在应用代码里也写
delimiter。 那是客户端命令,驱动不认,会直接报语法错。 - 以为
alter procedure能改逻辑。 它只能改特性,改逻辑得 drop 再 create。 - 用存储过程做建表、改表结构。 过程体里不许 DDL,要走动态 SQL,得不偿失。
自测三问(点开对答案)
1. `delimiter $$` 是给谁看的? —— 给客户端看的,不是给数据库看的。它临时改掉「语句结束符」,免得过程体里的分号把语句截断。 2. `in`、`out`、`inout` 分别是什么? —— `in` 只进不出(默认),`out` 只出不进(用 @变量接),`inout` 两头都通。 3. 本沙箱里「存储过程」的等价物是什么?差别在哪? —— 宿主语言的函数:参数当 `in`、返回值当 `out`、多条 `execute` 当过程体。 差别是每 `execute` 一次就一趟网络,而存储过程全程在数据库进程里。存储过程要人叫它才跑,下一篇讲一种不用叫、能直接写进 select 的:13.3 存储函数。
- ☐ (本篇完,下一篇 13.3 存储函数)