🛢️ 第14章 存储过程与存储函数
📄 一句话概括

DROP PROCEDURE 和 DROP FUNCTION 是两个命令,不能混用。 删掉之后调用报 1305「不存在」,而且往往不是当场发现,是半夜定时任务报错。 IF EXISTS 在批量脚本里几乎必加,不加会在第一个不存在的名字上中断。

📝 本章和前面几章的关系

14.5 只有一句话:DROP PROCEDURE。这一篇补两件事:删掉之后会怎样、删之前该查什么。 这一篇是第 14 章的收尾,后面 14.6 小结和 14.7 实践与练习并进本篇末尾。 上一篇讲改,这一篇讲删。两篇合起来才算「过程的日常维护」。

一、DROP 的写法

DROP PROCEDURE p_list_cat;
DROP FUNCTION goods_level;
DROP PROCEDURE IF EXISTS p_list_cat;

第一条和第二条不能互换:过程和函数是两个命名空间,DROP PROCEDURE 删不掉函数。 这一点容易错,因为建的时候 CREATE 后面也是两种写法,但那时错了会立刻报错。 第三条在脚本里几乎必加。 不加的话,删一个不存在的名字会中断整个脚本,后面的语句全都不执行。

二、删掉之后调用:报「不存在」

删之前正常返回,删之后报 PROCEDURE p_tmp does not exist。 MySQL 那边是错误码 1305。

时机 调用结果
删除之前 正常返回
删除之后 报 1305「不存在」

麻烦的是这类错误往往不是马上出现。 删的时候一切正常,出问题的时候是半夜的定时任务、早上的报表生成—— 中间隔了几个小时,中间还可能有别的变更,排查会很绕。 所以删之前要确认「谁在调」,而不是「现在没人抱怨」。

三、删之前先查谁在调

谁可能引用它 怎么查
应用代码 全局搜 CALL 过程名
其它过程 搜 CALL 和过程名
视图/触发器 放不进过程,可排除
定时任务 查事件调度器里的 CALL

第三行是个好消息:视图和触发器里只能放函数,放不进过程,所以排查范围比删表小得多。 但函数就不一样了——函数能进视图定义,删函数之前得把视图也捋一遍。 第二行要注意:过程调过程很常见,删底层那个之前得先把上层理一遍。 这层依赖数据库不帮你记,只能靠搜代码。

四、批量删:脚本里的稳妥写法

一次删三个,其中 p_nope 根本不存在:前两个删掉,第三个跳过,脚本继续。 这就是 IF EXISTS 的作用;不带它的话,循环到第三个就中断了。

过程名 结果
p_a 删掉了
p_b 删掉了
p_nope 不存在,跳过

实践里还会加两道保险:删之前先 SHOW PROCEDURE STATUS LIKE '前缀%' 列一遍, 删之后再用同一条查一次确认清空。 第 14 章到这里结束。这一章真正要带走的是三条: 过程是「收进名字里的 SQL」,函数是「能写进 SQL 的一个值」; DELIMITER 是客户端指令不是 SQL;改过程只能删了重写,中间有空档。 14.7 的练习可以挑这三道:写一个带 IN 和 OUT 的过程、把一段重复的 INSERT 改写成过程、 给一个已经上线的过程做「改名切换」的迁移。 三道都动手做过,这一章就算真读完了。

五、动手 Demo:删之前、删之后、批量删

这一段把 DROP 的三种写法贴成原码,再用复刻版跑一遍:删之前调一次、删之后再调一次, 最后演示批量删时 IF EXISTS 怎么救场。

REG = {}

def create(name, fn, code):
    REG[name] = {"fn": fn, "code": code}

def call(name, *a):
    if name not in REG:
        raise KeyError(u"PROCEDURE %s does not exist" % name)
    return REG[name]["fn"](*a)

def drop(name):
    if name not in REG:
        raise KeyError(u"PROCEDURE %s does not exist" % name)
    del REG[name]

# ① 删之前能调,删之后不能
create(u"p_tmp", lambda n: list(range(n)), u"CREATE PROCEDURE p_tmp(...)")
print(call(u"p_tmp", 3))
drop(u"p_tmp")
try:
    call(u"p_tmp", 3)
except KeyError as e:
    print(e.args[0])

# ② 批量删:不存在的那个要能跳过
for nm in (u"p_a", u"p_b", u"p_c"):
    create(nm, lambda: None, u"CREATE PROCEDURE %s()" % nm)
print(len(REG))
for nm in (u"p_a", u"p_b", u"p_nope"):
    try:
        drop(nm)
        print(nm, u"删掉了")
    except KeyError:
        print(nm, u"不存在,跳过")
print(len(REG))

实跑输出:

--------------------------------------------------------------------
第六十四课:删过程,以及删之前该看什么
--------------------------------------------------------------------

  **教材 14.5 讲 `DROP PROCEDURE`,一句话。**
  **这一课补两件教材没细说的事:删掉之后调用会怎样、删之前该查什么。**

  一、DROP 的写法
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | DROP PROCEDURE p_list_cat;
  教材原码,删过程


  [教材原码 · 本沙箱不执行]
  | DROP FUNCTION goods_level;
  教材原码,删函数要用 DROP FUNCTION,不能混


  [教材原码 · 本沙箱不执行]
  | DROP PROCEDURE IF EXISTS p_list_cat;
  教材原码,加 IF EXISTS 之后,删不存在的也不报错

  **第一条和第二条不能互换:过程和函数是两个命名空间,`DROP PROCEDURE` 删不掉函数。**
  **第三条在脚本里几乎必加,不然重复执行会中断。**

  二、删掉之后调用:报「不存在」
  --------------------------------------------------------------------

  时机      调用结果
  ----------------------------------------
  删除之前  正常,返回 2 行
  删除之后  PROCEDURE p_tmp does not exist
  **MySQL 那边对应的是错误码 1305:`PROCEDURE shop.p_tmp does not exist`。**
  **数字记不住没关系,记住「删了之后调用会报不存在」这件事就够了。**
  **麻烦的是这类错误往往不是马上出现:定时任务半夜跑、报表早上生成,**
  **删的时候一切正常,出问题的时候已经过了几个小时。**

  三、删之前先查谁在调
  --------------------------------------------------------------------

  谁可能引用它  怎么查
  ----------------------------------------
  应用代码      全局搜 `CALL 过程名`
  其它过程      搜 `CALL` 和过程名
  视图/触发器  过程不能被它们调用,可排除
  定时任务      查事件调度器里的 `CALL`
  **第三行是个好消息:视图和触发器里放不进过程(只能放函数),**
  **所以排查范围比删表小得多。**
  **第二行要注意:过程调过程是常见写法,删底层那个之前得先把上层捋一遍。**
  **这一层关系数据库不帮你记,只能靠搜代码。**

  四、批量删:脚本里的稳妥写法
  --------------------------------------------------------------------
  **先建三个,验证一下批量删:**

  过程名  结果
  --------------------
  p_a     删掉了
  p_b     删掉了
  p_nope  不存在,跳过

  时机    剩下几个
  ----------------
  删之前  3
  删之后  1
  **`p_nope` 那一行被跳过了,脚本继续往下跑——这就是 `IF EXISTS` 的作用。**
  **不带它的话,循环到第三个就中断,后面该删的都没删。**
  **第 14 章到这里结束。这一章真正要带走的是三条:**
  **过程是「收进名字里的 SQL」,函数是「能写进 SQL 的一个值」;**
  **`DELIMITER` 是客户端指令不是 SQL;改过程只能改了重写,中间有空档。**

第一组两行:删之前拿到 [0, 1, 2],删之后报 PROCEDURE p_tmp does not exist。 MySQL 那边就是错误码 1305,一字不差的语义。 第二组第一个数是 3,说明三个都建上了;后面三行是批量删的过程,p_nope 被跳过。 最后一组那个数是 1:p_a、p_b 删掉了,p_c 不在删除名单里,还留着。 注意 p_nope 那一行没有中断脚本——这就是 IF EXISTS 的价值,也是它能进教科书的原因。

30 秒实验

  1. 把批量删里 try/except 去掉,看第三个名字会不会让整个循环停下。
  2. 删掉一个过程之后再建同名的,看是不是「全新」的一个(有没有残留)。
  3. 写一个「先建新名字、切调用、再删旧名字」的三步脚本,跑一遍。

常见误区

  1. 用 DROP PROCEDURE 去删函数,报错之后以为名字写错了。
  2. 批量删不带 IF EXISTS,第一个不存在就整段中断。
  3. 删之前只在测试环境验证过,生产上还有定时任务在调。
自测三问(点开对答案) 1. 删掉过程之后调用会报什么? —— MySQL 报错误码 1305,提示这个 PROCEDURE 不存在。 麻烦的是它往往延迟出现,半夜定时任务跑的时候才暴露。 2. 为什么批量删要加 `IF EXISTS`? —— 不加的话,删到一个不存在的名字就会中断执行,后面的语句全不跑。 加了这个关键字,不存在的会被跳过,脚本能继续。 3. 删过程之前要排查哪些引用? —— 应用代码里的 `CALL`、其它过程里的 `CALL`、定时任务里的 `CALL`。 视图和触发器里放不进过程,可以排除——但函数不行,函数能进视图定义。

第 14 章到这里结束。下一篇进第 15 章:触发器,让数据库自己在写入前后做事。

相关笔记