🪟 第11章 存储过程
📄 一句话概括

三段结构:过程头(名字 + 参数)、过程体(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 秒实验

  1. 把 SET NOCOUNT ON 删掉,在 SSMS 里跑一次(或者想一想)。 每条语句都会多一行「N 行受影响」——语句越多越明显。
  2. 把过程名改成 sp_low_stock 再建一次,查一下到底建在哪个库里。 SQL Server 会先去 master 找同名的系统过程——别用 sp_ 开头就是这个原因。
  3. 试着把 CREATE PROCEDURE 和一句 SELECT 拼在同一批里执行。 会报 Incorrect syntax near 'CREATE'——这就是「必须独占一批」。

常见误区

  1. 「SET NOCOUNT ON 是可有可无的优化」。它影响了客户端拿到的行数(变成 -1)。 老代码按行数判断成功与否的,加之前要确认。
  2. 「过程名用 sp_ 开头显得专业」。恰恰相反—— SQL Server 会先去 master 找系统过程,多绕一圈还容易撞名。 自己的过程用 proc_ 或者业务前缀。
  3. 「建过程跟建表一样,拼在一条脚本里发就行」。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 经常被混为一谈,其实一个带业务数据、一个报状态码。

相关笔记