查看分两步:sys.procedures 只给名字,要看正文得用 sp_helptext 或查 sys.sql_modules。
改定义用 ALTER PROCEDURE,它不丢权限;DROP 再建,之前授过的权限就没了。
嵌套最多 32 层,但真正的麻烦是排错——外层只说「内层失败」,你得一层层往下找。
11.2a 建、11.2b 加参数,这一节讲「建完之后怎么管」——查看、修改、删除、重编译、嵌套调用。
教材 11.3 分了五六小节,「小结」和「实践与练习」按惯例并进本篇末尾。
本沙箱还是那套演法: T-SQL 原码照贴标注不执行,
查看/修改/删除都用 sys_proc 这张自建表真跑一遍——它对应 sys.procedures,
区别是这张表是我们自己建的,不是数据库自带的。
一、查看:先看名字,再看正文
SELECT name FROM sys.procedures ORDER BY name;
EXEC sp_helptext 'proc_low_stock';
GO
sys.procedures 只给名字和几个属性,不给正文。 要看正文有两条路:
sp_helptext(简单,直接打出来) 或者 查 sys.sql_modules 的 definition 列(能加工)。
演示里查出来两个过程,正文长度分别是 104 和 68。正文长度这个数挺有用——
两个过程名字不同、长度一样,八成是复制粘贴忘了改。
还可以用 OBJECT_DEFINITION(OBJECT_ID('proc_name'))。三个办法都能看正文,
区别是要「人看」还是「程序处理」。
二、修改:ALTER PROCEDURE
ALTER PROCEDURE proc_low_stock
AS
BEGIN
SET NOCOUNT ON;
SELECT id, gname, stock, 100 - stock AS 建议补货量
FROM tb_goods WHERE is_on = 1 AND stock < 20;
END;
GO
演示里把补货算法从 50 - stock 改成 100 - stock。
客户端一个字都没改,照样 EXEC proc_low_stock,拿到的建议量自动变大了。
这就是「逻辑集中」的实际样子。 如果这段算法写在五个客户端里,
你得改五处、发五个版本、还得盯五个地方有没有升级。
ALTER 不丢权限。 换成 DROP + CREATE,之前授过的 EXEC 权限就全没了,
而且中间那段时间过程不存在,正在调的客户端会直接报错。
三、删除:DROP PROCEDURE
DROP PROCEDURE proc_restock;
GO
删之前先想清楚:客户端是不是还在调它? 删完那边就报「找不到对象」,
而且这种事往往发生在半夜的业务高峰。
稳妥的做法是「先改名,再观察」。 把 proc_restock 改成 proc_restock_bak_20261031,
观察一周没人报错,再真删。
还有一条:删过程不会删它写过的数据。 过程只是逻辑,
它执行时插进去的行还在表里——别指望删了过程就能「撤销」。
四、重新编译:WITH RECOMPILE
EXEC sp_recompile 'proc_low_stock';
-- 或者建的时候写死在定义里:
CREATE PROCEDURE proc_x WITH RECOMPILE AS ...
GO
存储过程第一次执行时会把执行计划缓存起来,以后直接复用。 多数时候这是好事——
省掉了每次编译的开销。
但有一种情况会坏事,叫「参数嗅探」。 过程里根据参数走不同分支,
第一版计划是为「小范围查询」生成的(比如只查一个类别),
后来传进来的参数要查全表——缓存的那版计划就非常不合适。
重新编译能解一时之急(sp_recompile 让下次执行重新生成计划),
但它治标不治本。 根子在过程里的动态 SQL 或分支写法——
该拆成两个过程就拆,该用 OPTION (RECOMPILE) 就用。
WITH RECOMPILE 写在定义里,就是「每次执行都重新编译」。
这解决了参数嗅探,代价是每次都多花编译时间——高频调用的过程别这么干。
五、嵌套调用:能少用就少用
CREATE PROCEDURE proc_outer
AS
BEGIN
EXEC proc_low_stock;
SELECT COUNT(*) FROM tb_goods;
END;
GO
嵌套最多 32 层(SQL Server 的限制)。但层数不是主要问题——真正的麻烦是排错。
外层报错说「内层执行失败」,你还得去内层找;内层再调一层,找起来更费劲。
而且过程是存在数据库里的,没法像代码那样断点调试(得靠 PRINT 和临时表记日志)。
建议:嵌套不超过两层,而且每层都写清楚「失败时返回什么状态码」。演示里外层调内层,
两步各返回 1 行——业务上没问题,但一旦中间那步出错,你得靠状态码一路倒推是哪一层挂的。
清理时的注意点: 看着「没人用」的过程,
可能还有旧客户端在调——删之前先查日志(sys.dm_exec_procedure_stats 有执行次数),
或者先改名观察一周。
六、动手 Demo:查看、修改、删除、重编译与嵌套调用
这一段在 sys_proc 表上把管理动作走一遍:
查出两个过程 → 改一个的定义 → 删掉另一个 → 看剩下的。
最后演嵌套调用:外层过程里调内层,两步各返回一行。
import sqlite3
c = sqlite3.connect(u":memory:")
# ... 建 sys_proc 目录表,装两个过程 ...
# 查看:名字 + 正文长度
print(c.execute(u"SELECT pname, length(body) FROM sys_proc ORDER BY pname").fetchall())
# 修改:ALTER 的等价做法
c.execute(u"UPDATE sys_proc SET body = replace(body, '50 - stock', '100 - stock'), "
u"modified = datetime('now','localtime') WHERE pname = 'proc_low_stock'")
# 删除:DROP 的等价做法
c.execute(u"DELETE FROM sys_proc WHERE pname = 'proc_restock'")
print(c.execute(u"SELECT pname FROM sys_proc").fetchall())
# 嵌套:外层过程里调内层
outer = (u"SELECT id, gname, stock FROM tb_goods WHERE is_on = 1 AND stock < 20; "
u"SELECT COUNT(*) FROM tb_goods")
for i, stmt in enumerate(outer.split(u"; "), 1):
rows = c.execute(stmt).fetchall()
print(u"第 %d 步返回 %d 行" % (i, len(rows)))
实跑输出:
--------------------------------------------------------------------
管理存储过程:查看、修改、删除、重编译、嵌套
--------------------------------------------------------------------
① 查看:有哪些过程、某个过程的正文
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT name FROM sys.procedures ORDER BY name;
| EXEC sp_helptext 'proc_low_stock';
| GO
1> SELECT pname, length(body) AS 正文长度 FROM sys_proc ORDER BY pname
pname 正文长度
------------------------
proc_low_stock 104
proc_restock 68
(2 行受影响)
sys.procedures 只给名字,要看正文得查 sys.sql_modules 或者用 sp_helptext。
② 修改:ALTER PROCEDURE
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| ALTER PROCEDURE proc_low_stock
| AS
| BEGIN
| SET NOCOUNT ON;
| SELECT id, gname, stock, 100 - stock AS 建议补货量
| FROM tb_goods WHERE is_on = 1 AND stock < 20;
| END;
| GO
1> UPDATE sys_proc
SET body = replace(body, '50 - stock', '100 - stock'),
modified = datetime('now', 'localtime')
WHERE pname = 'proc_low_stock'
1> SELECT pname, modified FROM sys_proc WHERE pname = 'proc_low_stock'
pname modified
-----------------------------------
proc_low_stock 2026-10-31 03:06:53
(1 行受影响)
补货量的算法从 50 改成 100,只改过程,客户端一个字都不用动——这就是「逻辑集中」。
! ALTER 不丢权限;换成 DROP + CREATE,之前授过的 EXEC 权限就没了。
③ 删除:DROP PROCEDURE
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| DROP PROCEDURE proc_restock;
| GO
1> DELETE FROM sys_proc WHERE pname = 'proc_restock'
1> SELECT pname FROM sys_proc ORDER BY pname
pname
--------------
proc_low_stock
(1 行受影响)
删掉了。删之前要想清楚:客户端是不是还在调它?删完那边就报「找不到对象」。
稳妥的做法是先改名(相当于下线),观察一周没人报错,再真删。
④ 重新编译:WITH RECOMPILE
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| EXEC sp_recompile 'proc_low_stock';
| -- 或者建的时候写死在定义里:
| CREATE PROCEDURE proc_x WITH RECOMPILE AS ...
| GO
存储过程第一次执行时会把执行计划缓存起来,以后直接复用。多数时候这是好事。
但有一种情况会坏事:过程里根据参数走不同的分支,第一版计划是为「小范围查询」生成的,
后来传进来的参数要查全表——缓存的那版计划就非常不合适。这叫「参数嗅探」。
遇到这种情况,重新编译能解一时之急。但它治标不治本,根子在过程里的动态 SQL 或分支。
⑤ 嵌套调用:过程里调过程
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| CREATE PROCEDURE proc_outer
| AS
| BEGIN
| EXEC proc_low_stock; -- 内层
| SELECT COUNT(*) FROM tb_goods; -- 再干点别的
| END;
| GO
1> INSERT INTO sys_proc (pname, body, created, modified) VALUES (?,?,?,?)
1> EXEC proc_outer -- 内层 proc_low_stock 换成了等价语句
第 1 步返回 1 行,首行 (1006, '打印机', 12)
第 2 步返回 1 行,首行 (6,)
嵌套最多 32 层(SQL Server 的限制),但嵌套真正的问题是排错变难:
外层报错说「内层执行失败」,你还得去内层找;内层再调一层,找起来更费劲。
建议:嵌套不超过两层,而且每层都写清楚「失败时返回什么状态码」。
现在库里只剩 proc_low_stock 和 proc_outer 两个,其中 proc_outer 是刚才为了演嵌套建的。
清理时要注意:看着「没人用」的过程,可能还有旧客户端在调——删之前先查日志或者先改名观察。
第一段两个数:proc_low_stock 104、proc_restock 68。
正文长度是一个很快的健康检查——如果两个过程名字不同、长度一模一样,
很可能是复制粘贴时忘了改里面的表名。
第二段那个 replace() 是在模拟 ALTER PROCEDURE。真实环境里你会把整个定义重写一遍,
而不是替换其中一个片段——演示这么写,只是为了让「改了一处」这件事看得见。
第三段剩下一个过程。 删之前那一步没做的检查(谁在调它)得在真实环境里补上——
本沙箱里没有客户端,所以看不出后果。
最后两步各返回 1 行:先是打印机那一行,然后是总行数 6。
嵌套本身没难度,难的是中间某一步失败时,你怎么知道是第几步挂的。
所以每层都返回状态码,是最省事的做法。
30 秒实验
- 给
sys_proc加一列exec_count,每次「调用」时加一。 这就是 SQL Server 里sys.dm_exec_procedure_stats干的事——删过程之前先看这个。 - 把
proc_low_stock的50 - stock改成100 - stock之后,重新EXEC一次,对比结果。 建议补货量从 38 变成 88——客户端没改一行代码。 - 想一想:如果外层过程调内层,而内层
RETURN 2表示「重名」,外层该怎么处理? 要么原样往上RETURN 2,要么翻译成自己的错误码——但一定要有约定, 否则调用方拿到 2 根本不知道是哪一层出的问题。
常见误区
- 「改过程用
DROP+CREATE就行」。权限会丢,中间还有一段「过程不存在」的空窗期。 有ALTER就用ALTER。 - 「过程没人调,删掉算了」。你怎么知道没人调? 先查执行统计或者先改名观察。 半夜报错的往往是这种事。
- 「
WITH RECOMPILE能解决性能问题」。它只解决「计划缓存不合适」, 而且每次执行都要多花编译时间。 高频调用的过程加它,反而更慢。
自测三问(点开对答案)
1. 怎么看到某个存储过程的正文? —— **三条路:`sp_helptext 'proc_name'`、查 `sys.sql_modules` 的 `definition` 列、** **或者 `OBJECT_DEFINITION(OBJECT_ID('proc_name'))`。** **要人看用第一个,要程序处理用后两个。** 2. `ALTER PROCEDURE` 比 `DROP` + `CREATE` 好在哪? —— **不丢权限,也没有「过程不存在」的空窗期。**`DROP` 之后再建, 之前 `GRANT` 的 `EXEC` 权限全没了。 3. 「参数嗅探」是什么?怎么办? —— **过程缓存了为「小范围查询」生成的计划,后来传进来的参数要查全表,那版计划就不合适了。** **临时办法是 `sp_recompile`,根治要改过程里的分支或动态 SQL 写法。**第 11 章到此结束。
下一篇进第 12 章 12.1,讲触发器:
它是「自动执行的过程」——你不用 EXEC,数据库在 INSERT/UPDATE/DELETE 的时候替你调。
- ☐ (本篇完,下一篇 12.1 触发器概述)