三段结构:过程头(名字 + 参数)、过程体(BEGIN ... END)、SET NOCOUNT ON。
SET NOCOUNT ON 几乎每个生产过程第一行都是它——挡掉每条语句一条的「N 行受影响」消息。
CREATE PROCEDURE 必须是一个批的第一句,所以传统写法是前后各来一个 GO。
11.1 讲「为什么要用」,这一节讲「怎么写」——结构、第一个能跑的过程、三个坑。
教材 11.2 分两小节:用 CREATE PROC 创建、存储过程的三段结构。
PROC 是 PROCEDURE 的简写,两个都能用,习惯上写全。
本沙箱没有过程对象,所以演示里用 Python 把「存过程 + 调用过程」这套语义复刻出来,
T-SQL 原码逐段贴出并标注不执行。
一、三段结构
CREATE PROCEDURE proc_name
@arg1 INT,
@arg2 NVARCHAR(20) = 'default'
AS
BEGIN
SET NOCOUNT ON;
-- 过程体:一堆 SQL
END;
GO
| 段 | 写什么 | 容易漏的 |
|---|---|---|
| 过程头 | CREATE PROCEDURE + 名字 + 参数列表 |
参数前那个 @ 不能省 |
| 过程体 | BEGIN ... END 包住的语句 |
不写 BEGIN/END 也能跑,但读起来乱 |
SET NOCOUNT ON |
不返回「N 行受影响」 | 漏了会让客户端多收一堆消息 |
AS 是分界线:上面是「过程头」,下面是「过程体」。 有人习惯把 AS 单独一行,
有人写在名字后面——都行,但一份代码里最好统一。
二、SET NOCOUNT ON:为什么几乎每个过程都有它
SQL Server 每执行完一条语句,都会给客户端发一条「N 行受影响」。一条语句一条消息,
一个跑几十条语句的过程就是几十条消息。
这些消息本身没用,但会占网络、占客户端的解析时间。SET NOCOUNT ON 把它们挡掉——
只挡消息,不影响数据。
还有个坑: 有些客户端库靠「行数」判断操作是否成功。
开了 NOCOUNT 之后拿到的是 -1,按行数判断的代码就会误判。
所以改别人的老过程时,加这一句之前先看看调用方怎么判断的。
三、第一个过程:建、调、看结果
CREATE PROCEDURE proc_low_stock
AS
BEGIN
SET NOCOUNT ON;
SELECT id, gname, stock, 50 - stock AS 建议补货量
FROM tb_goods
WHERE is_on = 1 AND stock < 20
ORDER BY stock;
END;
GO
EXEC proc_low_stock;
GO
演示里这个过程返回 1 行:1006 号打印机,库存 12,建议补 38。
注意这个数值是 50 - stock 算出来的,不是写死的。下次库存变成 5,
建议量自动变 45——这就是「把逻辑存起来」的意义。
EXEC 也可以写成 EXECUTE。 调用的时候过程名后面不加括号(加了在某些情况下会报错),
参数用逗号隔开——这一点跟函数调用看起来像,但规则不一样。
四、过程体里可以有流程控制
存储过程不是「一条 SQL 的别名」。 它能写变量、IF、WHILE、事务:
CREATE PROCEDURE proc_check_stock
AS
BEGIN
SET NOCOUNT ON;
DECLARE @n INT;
SELECT @n = COUNT(*) FROM tb_goods WHERE is_on = 1 AND stock = 0;
IF @n > 0
PRINT '有 ' + CAST(@n AS VARCHAR(10)) + ' 个商品已断货';
ELSE
PRINT '没有断货商品';
END;
GO
DECLARE @n INT 声明变量,SELECT @n = ... 给变量赋值。
注意这里不是 SET @n = (SELECT ...)——两种写法都行,SELECT 那种更常见。
PRINT 是打印消息,不是返回结果集。 演示数据里唯一的零库存商品(移动硬盘)已经下架,
所以这个过程打印的是「没有断货商品」。
能写流程控制这件事,是存储过程和视图最大的区别。 视图只能存一句查询,
过程里可以有判断、循环、异常处理——它是「数据库里的程序」。
五、建的时候三个坑
| 坑 | 报错长什么样 | 怎么改 |
|---|---|---|
| 名字重了 | There is already an object named ... |
先 DROP 或改用 ALTER |
参数漏了 @ |
Must declare the scalar variable |
参数名前面加 @ |
GO 位置不对 |
Incorrect syntax near 'CREATE' |
CREATE PROC 必须是一批的第一句 |
第三条是新手最容易被绊住的。 CREATE PROCEDURE、CREATE VIEW、
CREATE TRIGGER 这类「建对象」的语句,必须独占一个批(前面不能有别的语句)。
所以传统写法是前后各来一个 GO。 前端工具(SSMS)看到 GO 就把前面攒的一批发出去,
下一批从这里重新开始——这就是「独占一批」的实现方式。
用程序发 SQL 的时候没这个概念,所以要自己把 CREATE PROCEDURE 单独作为一次调用。
这也是为什么很多 ORM 工具建过程会失败——它把建表和建过程拼在一起发了。
六、动手 Demo:三段结构、第一个过程与三个坑
这一段贴出三段结构的骨架,然后建一个真能跑的过程(用 sys_proc 存正文、取出再执行),
再演示过程体里的变量和 IF。
最后一张表列了建过程最容易踩的三个坑,每条都配了报错原文。
import sqlite3
c = sqlite3.connect(u":memory:")
# ... 建六表、装样例数据、建 sys_proc 目录表 ...
# 「建过程」:把过程体存进目录表
c.execute(u"INSERT INTO sys_proc (pname, body, created, modified) "
u"VALUES (?,?,datetime('now','localtime'),datetime('now','localtime'))",
(u"proc_low_stock",
u"SELECT id, gname, stock, 50 - stock AS 建议补货量 FROM tb_goods "
u"WHERE is_on = 1 AND stock < 20 ORDER BY stock"))
# 「调用过程」:取出正文再执行
body = c.execute(u"SELECT body FROM sys_proc WHERE pname = 'proc_low_stock'").fetchone()[0]
print(c.execute(body).fetchall())
# 过程体里的流程控制,用 Python 复刻同一个语义
n = c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE is_on = 1 AND stock = 0").fetchone()[0]
print(u"有 %d 个商品已断货" % n if n else u"没有断货商品")
实跑输出:
--------------------------------------------------------------------
创建存储过程:三段结构与第一次调用
--------------------------------------------------------------------
① 三段结构
--------------------------------------------------------------------
一个存储过程就三块:过程头(名字 + 参数)、过程体(BEGIN ... END)、返回。
[教材原码 · 本沙箱不执行]
| CREATE PROCEDURE proc_name
| @arg1 INT,
| @arg2 NVARCHAR(20) = 'default'
| AS
| BEGIN
| SET NOCOUNT ON;
| -- 过程体:一堆 SQL
| END;
| GO
| 段 | 写什么 | 容易漏的 |
|---|---|---|
| 过程头 | CREATE PROCEDURE 名字 + 参数列表 | 参数前那个 @ 不能省 |
| 过程体 | BEGIN ... END 包住的语句 | 不写 BEGIN/END 也能跑,但读起来乱 |
| SET NOCOUNT ON | 不返回「N 行受影响」 | 漏了会让客户端多收到一堆消息 |
SET NOCOUNT ON 这一句几乎每个生产存储过程的第一行都是它。
它挡掉的是每执行一条语句就发一次的「N 行受影响」消息——一千条语句就是一千条消息。
② 第一个过程:建、调、看结果
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| CREATE PROCEDURE proc_low_stock
| AS
| BEGIN
| SET NOCOUNT ON;
| SELECT id, gname, stock, 50 - stock AS 建议补货量
| FROM tb_goods
| WHERE is_on = 1 AND stock < 20
| ORDER BY stock;
| END;
| GO
| EXEC proc_low_stock;
| GO
本沙箱用 Python 复刻同一个语义:把过程体存进 sys_proc,调用时取出来执行。
1> INSERT INTO sys_proc (pname, body, created, modified)
VALUES ('proc_low_stock', 'SELECT id, gname, stock, 50 - stock AS 建议补货量
FROM tb_goods WHERE is_on = 1 AND stock < 20 ORDER BY stock', datetime('now',
'localtime'), datetime('now', 'localtime'))
1> EXEC proc_low_stock -- 本沙箱:取出 body 再执行
id gname stock 建议补货量
-------------------------------
1006 打印机 12 38
(1 行受影响)
这个过程和直接用 SELECT 的区别只有一个:语句存在库里,不用每次敲。
③ 过程体里可以有流程控制
--------------------------------------------------------------------
存储过程不是「一条 SQL 的别名」,它能写 IF、WHILE、变量、事务。
[教材原码 · 本沙箱不执行]
| CREATE PROCEDURE proc_check_stock
| AS
| BEGIN
| SET NOCOUNT ON;
| DECLARE @n INT;
| SELECT @n = COUNT(*) FROM tb_goods WHERE is_on = 1 AND stock = 0;
| IF @n > 0
| PRINT '有 ' + CAST(@n AS VARCHAR(10)) + ' 个商品已断货';
| ELSE
| PRINT '没有断货商品';
| END;
| GO
EXEC proc_check_stock -> 没有断货商品
演示数据里唯一的零库存商品(移动硬盘)已经下架了,所以结果是「没有断货商品」。
④ 建的时候常见的三个坑
--------------------------------------------------------------------
| 坑 | 报错长什么样 | 怎么改 |
|---|---|---|
| 名字重了 | There is already an object named ... | 先 DROP 或改用 ALTER |
| 参数漏了 @ | Must declare the scalar variable | 参数名前面加 @ |
| GO 位置不对 | Incorrect syntax near 'CREATE' | CREATE PROCEDURE 必须是一批的第一句 |
第三条是新手最容易被绊住的:CREATE PROCEDURE、CREATE VIEW、CREATE TRIGGER 这类
「建对象」的语句,必须独占一个批(前面不能有别的语句)。所以传统写法是前后各来一个 GO。
第一段把过程体存进去,第二段取出来执行——这就是「存起来」的全部魔法。
SQL Server 帮你把这一步做成了内置对象,本质没变:语句存在库里,调用时取出来跑。
第二段返回 1006 打印机 12 38。 38 是 50 - 12 算的,
库存一变它就跟着变——这就是「逻辑」和「数据」分开的好处。
第三段那句 if n else 是 Python 的写法,对应 T-SQL 的 IF @n > 0 ... ELSE ...。
语义一样:先算出一个数,再决定打印什么。
三个坑里,GO 那条最隐蔽。 它不报语法细节,只说 Incorrect syntax near 'CREATE',
新手往往以为是 CREATE 写错了,其实是前面那句没断开。
30 秒实验
- 把
SET NOCOUNT ON删掉,在 SSMS 里跑一次(或者想一想)。 每条语句都会多一行「N 行受影响」——语句越多越明显。 - 把过程名改成
sp_low_stock再建一次,查一下到底建在哪个库里。 SQL Server 会先去master找同名的系统过程——别用sp_开头就是这个原因。 - 试着把
CREATE PROCEDURE和一句SELECT拼在同一批里执行。 会报Incorrect syntax near 'CREATE'——这就是「必须独占一批」。
常见误区
- 「
SET NOCOUNT ON是可有可无的优化」。它影响了客户端拿到的行数(变成 -1)。 老代码按行数判断成功与否的,加之前要确认。 - 「过程名用
sp_开头显得专业」。恰恰相反—— SQL Server 会先去master找系统过程,多绕一圈还容易撞名。 自己的过程用proc_或者业务前缀。 - 「建过程跟建表一样,拼在一条脚本里发就行」。
CREATE PROCEDURE必须是批的第一句, 拼在一起就报语法错。 用程序发的时候要单独发一次。
自测三问(点开对答案)
1. 存储过程的三段结构是什么? —— **过程头**(`CREATE PROCEDURE` + 名字 + 参数)、**过程体**(`BEGIN ... END` 包住的语句)、 **`SET NOCOUNT ON`**(挡掉「N 行受影响」消息)。 2. 为什么 `CREATE PROCEDURE` 前后要各写一个 `GO`? —— **因为它必须是一个批的第一句。** `GO` 是客户端的分隔符, **看到它就把前面攒的一批发出去,下一批重新开始**——这样建对象语句就独占了一批。 3. `SET NOCOUNT ON` 有什么副作用? —— **客户端拿到的行数会变成 -1。** 有些老代码靠「行数」判断操作成功与否, **加了这一句就会被误判**——改别人的过程之前先看看调用方。下一篇 11.2b 讲参数:输入参数、输出参数、默认值,还有那个只能回一个整数的 RETURN。
OUTPUT 参数和 RETURN 经常被混为一谈,其实一个带业务数据、一个报状态码。
- ☐ (本篇完,下一篇 11.2b 带输入参数与输出参数的存储过程)