存储过程就是「把一批 SQL 存进数据库,起个名字」——客户端以后只传名字,不传语句。
它和表一样是数据库对象,存在同一套数据字典里:SQL Server 查 sys.procedures。
四条好处里最实用的是第三条:权限可以只开在过程上,客户端改不了表里的其他数据。
第 10 章的视图是「存一句 SELECT」,
这一章的存储过程是「存一批语句 + 流程控制」——从「存查询」升级到「存逻辑」。
教材 11.1 分三小节:概念、优点、类型。
这一节一句 SQL 都不用自己写,但它回答了一个关键问题:为什么要把逻辑放进数据库?
本沙箱没有存储过程这个对象(SQLite 不支持),所以演示分两层:T-SQL 原码照贴并标注不执行,
语义用 Python 复刻。其中「少往返」那一段是真测的。
一、概念:从「发脚本」到「发名字」
需求长这样: 每个月给「在售且库存小于 20」的商品出补货提醒,顺便算建议补货量。
三步 SQL,客户每个月跑一次。
问题在于「客户那边没有 DBA」。 发一个 .sql 文件让他自己在 SSMS 里打开、选中、
执行——迟早会出错:选错库、改了半句、忘了提交,出了事还得你去查。
存储过程把这批语句存进数据库,起个名字。 客户只要执行这个名字:
CREATE PROCEDURE proc_restock
AS
BEGIN
SET NOCOUNT ON;
SELECT id, gname, stock, 50 - stock AS 建议补货量
FROM tb_goods WHERE is_on = 1 AND stock < 20;
END;
GO
EXEC proc_restock;
GO
注意 CREATE 和 EXEC 之间那个 GO。 建对象和调用对象是两件事,
得分成两批发出去——6.6 说过,GO 是客户端的分隔符。
二、它到底存在哪:和表平级的对象
| 对象 | SQL Server 里存在哪 | 本沙箱对应物 |
|---|---|---|
| 表 / 视图 / 触发器 | sys.objects + sys.sql_modules |
sqlite_master |
| 存储过程 | sys.procedures + sys.sql_modules |
没有(用自建表顶替) |
| 过程里的语句文本 | sys.sql_modules.definition |
自建表的 body 列 |
「和表平级」这句话值得琢磨。 存储过程不是文件、不是配置,它是数据库里的一个对象——跟表、 视图、触发器一样,有名字、有属主、有权限。 所以它跟着数据库走。 备份数据库就带上了它,换台机器还原也还在, 不需要另外部署一份代码。 这是「逻辑放进数据库」最大的实际好处。 代价也在这儿: 改逻辑要连数据库改,没法走代码仓库的评审流程, 而且版本管理很麻烦——所以现代项目通常只把「稳定的、跟数据强相关的逻辑」放进去。
三、优点一:少传语句,少往返
客户端每发一条 SQL,都要走一遍:网络、解析、权限检查、生成执行计划。过程存进数据库之后,
语句只在建的时候传一次,以后每次只传一个名字和几个参数。
这个差别可以量出来。 演示里在本地内存库上比了两种写法:2000 条 INSERT,
一条一条发(每条各自提交)对比攒进一个事务里批量提交。
| 2000 条 INSERT 的两种写法 | 最快一次 |
|---|---|
| 一条一条发(每条各自提交) | 8.0 ms |
| 批量发(一个事务里) | 2.7 ms |
差 3 倍。 而且这还全在本机内存里、没有网络——真走网络时差距只会更大。
但口径要说清楚: 这里量到的是「每次提交」的开销,不是「解析」的开销。
在没有网络的本地库上,提交就是最像「一次往返」的那一步。
还有一句得说明白:存储过程并不能把两千条 INSERT 变成一次提交。该提交的还得提交。
它省的是「传语句、解析、查权限、生成计划」这些每次都要重复的活儿。
四、优点二到四
| 优点 | 具体是什么 |
|---|---|
| 执行计划复用 | 第一次编译后缓存起来,后面直接拿来用 |
| 减少网络流量 | 只传过程名和参数,不传几百行 SQL |
| 权限只开在过程上 | 给 EXEC 权限,不给表的写权限 |
| 逻辑集中 | 改规则只改过程,不用挨个改客户端 |
第三条常被低估。 假设有个「转账」过程,客户端只能执行它,
没有 UPDATE 账户表的权限——那他就没法偷偷改余额,
只能走「检查余额、扣款、加款、记流水」这一整套。
这比「在代码里写检查」可靠得多。 代码可以被绕过(换个客户端、直连数据库),权限绕不过去。
第四条是维护上的价值。 同一条业务规则如果散在五个客户端里,
早晚会不一致——改了三个忘了两个,报表数字就对不上。放进过程里,只有一份。
五、类型:三类,其中一类要当心
| 类型 | 说明 | 典型用途 |
|---|---|---|
| 系统存储过程 | sp_ 开头,SQL Server 自带 |
sp_helpdb、sp_helptext |
| 扩展存储过程 | xp_ 开头,调外部程序 |
xp_cmdshell(危险,默认关) |
| 用户自定义存储过程 | 自己建的,最常用 | 业务逻辑 |
xp_ 开头的那类要特别小心。 它们能执行操作系统命令——等于把整台服务器交给能调用它的人。
xp_cmdshell 默认是关的,这不是「保守」,是「保命」。需要定时跑外部任务,
另外写一个受控的服务去做,别开这个口子。
自己建过程时也别用 sp_ 开头。 SQL Server 会先去 master 库里找同名的系统过程,
找不到才找你建的——多绕一圈,还容易撞名。
六、动手 Demo:把一批 SQL 存起来,以及「少往返」的实测
这一段先用一张自建表 sys_proc 演「过程存进数据库」这件事——
它对应 SQL Server 的 sys.procedures,区别是这张表是我们自己建的。
然后真测一次「一条一条提交」和「批量提交」的差距,再列一下三类过程和四条好处。
import sqlite3, time
c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE sys_proc (id INTEGER PRIMARY KEY AUTOINCREMENT,"
u" pname TEXT NOT NULL UNIQUE, body TEXT NOT NULL,"
u" created TEXT, modified TEXT)")
# 「建过程」= 把过程体存进目录表
c.execute(u"INSERT INTO sys_proc (pname, body, created, modified) "
u"VALUES (?,?,datetime('now','localtime'),datetime('now','localtime'))",
(u"proc_restock", u"SELECT id, gname, stock FROM tb_goods "
u"WHERE is_on = 1 AND stock < 20"))
print(c.execute(u"SELECT pname, length(body) FROM sys_proc").fetchall())
# 「少往返」的实测:逐条提交 vs 批量提交
c2 = sqlite3.connect(u":memory:", isolation_level=None)
c2.execute(u"CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
def one_by_one():
for i in range(2000):
c2.execute(u"INSERT INTO t (v) VALUES (?)", (u"v%d" % i,))
def batch():
c2.execute(u"BEGIN")
c2.executemany(u"INSERT INTO t (v) VALUES (?)", [(u"v%d" % i,) for i in range(2000)])
c2.execute(u"COMMIT")
print(u"逐条 %.1f ms,批量 %.1f ms" % (clock(one_by_one), clock(batch)))
实跑输出:
--------------------------------------------------------------------
存储过程是什么:把一批 SQL 存进数据库
--------------------------------------------------------------------
① 一段反复要跑的 SQL
--------------------------------------------------------------------
需求:每个月给「在售且库存小于 20」的商品补货提醒,顺便把状态记下来。
这段逻辑有三步:查出要补货的、算建议补货量、写一条日志。客户每个月跑一次。
1> SELECT id, gname, stock FROM tb_goods WHERE is_on = 1 AND stock < 20
id gname stock
-------------------
1006 打印机 12
(1 行受影响)
三步写成一个脚本没问题,但客户那边没有 DBA——发个 .sql 文件让他自己跑,迟早出错。
存储过程就是把这批语句存进数据库,起个名字,他只要执行名字就行。
[教材原码 · 本沙箱不执行]
| CREATE PROCEDURE proc_restock
| AS
| BEGIN
| SET NOCOUNT ON;
| SELECT id, gname, stock, 50 - stock AS 建议补货量
| FROM tb_goods WHERE is_on = 1 AND stock < 20;
| END;
| GO
| EXEC proc_restock;
| GO
! 本沙箱没有存储过程对象,上面这段一行都跑不了。下面用等价手段把「存起来」演一遍。
② 「存起来」到底存在哪
--------------------------------------------------------------------
| 对象 | SQL Server 里存在哪 | 本沙箱对应物 |
|---|---|---|
| 表 / 视图 / 触发器 | sys.objects + sys.sql_modules | sqlite_master |
| 存储过程 | sys.procedures + sys.sql_modules | 没有(用自建表顶替) |
| 过程里的语句文本 | sys.sql_modules.definition | 自建表的 body 列 |
1> INSERT INTO sys_proc (pname, body, created, modified)
VALUES ('proc_restock', 'SELECT id, gname, stock, 50 - stock AS 建议补货量 FROM
tb_goods WHERE is_on = 1 AND stock < 20', datetime('now', 'localtime'),
datetime('now', 'localtime'))
1> SELECT id, pname, length(body) AS 正文长度, created FROM sys_proc
id pname 正文长度 created
-----------------------------------------------
1 proc_restock 89 2026-10-31 03:06:53
(1 行受影响)
SQL Server 那边查同样信息的语句是:
SELECT name, OBJECT_DEFINITION(object_id) FROM sys.procedures。
重点是「过程和表存在同一套数据字典里」——存储过程也是数据库对象,跟表平级。
③ 优点一:少传语句,少往返
--------------------------------------------------------------------
客户端每发一条 SQL 都要经过网络、解析、权限检查、生成执行计划。
存进数据库之后,语句只传一次(建的时候),以后每次只传一个名字。
下面测的不是网络,是「每条语句各自提交一次」和「攒一批一起提交」的差别。
注意口径:这里量到的是「每次提交的开销」,不是「解析」的开销——在没有网络的本地内存库上,
提交就是最像「一次往返」的那一步。
| 2000 条 INSERT 的两种写法 | 最快一次(毫秒) |
|---|---:|
| 一条一条发(每条各自提交) | 7.8 |
| 批量发(一个事务里) | 3.0 |
差 3 倍。这还全在本机内存里,没有网络——真走网络时差距只会更大。
存储过程省下的正是这一截:把「两千次交互」压成「一次调用」。
但要说清楚:存储过程并不能把两千条 INSERT 变成一次提交,该提交的还得提交。
它省的是「传语句、解析、查权限、生成计划」这些每次都要重复的活儿。
④ 优点二到四
--------------------------------------------------------------------
| 优点 | 具体是什么 |
|---|---|
| 执行 plan 复用 | 第一次编译后缓存起来,后面直接拿来用 |
| 减少网络流量 | 只传过程名和参数,不传几百行 SQL |
| 权限可以只开在过程上 | 给 EXEC 权限,不给表的写权限 |
| 逻辑集中 | 改规则只改过程,不用挨个改客户端 |
第三条常被低估:客户端只能「执行这个过程」,改不了表里的其他数据。
第四条是维护上的价值——同一条业务规则散在五个客户端里,早晚会不一致。
⑤ 类型:三类
--------------------------------------------------------------------
| 类型 | 说明 | 典型用途 |
|---|---|---|
| 系统存储过程 | sp_ 开头,SQL Server 自带 | sp_helpdb、sp_helptext |
| 扩展存储过程 | xp_ 开头,调外部程序 | xp_cmdshell(危险,默认关) |
| 用户自定义存储过程 | 自己建的,最常用 | 业务逻辑 |
! xp_ 开头的那类要特别小心:它能执行操作系统命令,等于把服务器交给调用者。
除非万不得已,别开 xp_cmdshell。要执行外部任务,另写一个受控的服务去做。
第一段返回 ('proc_restock', 68)——过程名加正文长度。
SQL Server 里查同样信息是 SELECT name FROM sys.procedures,
加 OBJECT_DEFINITION(object_id) 才能看到正文。
第二段那两个数就是本篇唯一的实测:8.0 对 2.7。
但别把它理解成「存储过程快 3 倍」——它测的是「每次提交」的开销,
而存储过程省的是另一个东西(传语句、解析、查权限)。
要说清这两个概念的区别: 事务解决的是「一批写操作要么全成要么全败」,
存储过程解决的是「一批语句不用反复传、反复编译」。 两个正交,可以一起用。
最后那张类型表里 xp_ 那一行要看一眼。 它能执行系统命令,
一个参数没过滤干净,就是服务器被接管。 默认关着是有原因的。
30 秒实验
- 想一想:为什么「给
EXEC权限、不给表权限」比「在代码里检查」更可靠? 因为代码能被绕过(换个客户端直连数据库),权限绕不过去。 - 把演示里 2000 改成 20000,重跑。 两个数的差距会拉大还是缩小?先猜再跑——这类直觉只能靠实测校正。
- 在
sys_proc里查一下body的长度,再对比SELECT语句本身的字符数。 每次调用省下的就是这个长度的传输量,乘上调用次数就是省下的流量。
常见误区
- 「存储过程一定比直接 SQL 快」。不一定。 它的价值主要在「少往返、权限收口、逻辑集中」, 单条复杂查询写成过程,性能跟直接写一模一样。
- 「过程存进数据库就不用管版本了」。恰恰相反——过程最难做版本管理,改了什么、谁改的、 什么时候改的,不上心的话根本查不到。 现在多数团队会给过程脚本也建个仓库。
- 「
xp_cmdshell方便,开着吧」。它等于给调用者一个命令行。 一个拼接漏洞就是服务器沦陷,默认关着,别去开。
自测三问(点开对答案)
1. 存储过程存在数据库的什么地方? —— **和表平级,存在数据字典里。** SQL Server 是 `sys.procedures` 加 `sys.sql_modules`, **所以备份数据库就带上了它,不用另外部署。** 2. 「少往返」到底省的是什么? —— **省的是「传语句、解析、查权限、生成执行计划」这些每次都要重复的活儿。** **它不能把你的一千条 `INSERT` 变成一次提交**——该提交的还得提交。 3. 为什么权限要开在过程上,而不是表上? —— **因为权限绕不过去,代码可以。** 只给 `EXEC`、不给表写权限, **客户端就只能走过程里那套完整逻辑,没法偷偷改别的数据。**下一篇 11.2a 讲怎么建:
过程头、过程体、SET NOCOUNT ON 三段结构,以及建的时候最容易踩的三个坑(名字重了、
参数漏 @、GO 位置不对)。
- ☐ (本篇完,下一篇 11.2a 创建存储过程)