🪟 第6章 SQL基础
📄 一句话概括

DECLARE 声明,SET 或 SELECT 赋值;SELECT 赋值遇到多行只取最后一行,不报错。 变量只在「当前批处理」活着——GO 一分批,前面声明的变量后面就看不见了。 @@ 开头的是系统函数,只读;@@ROWCOUNT 用完就变,要立刻存下来。

📝 本章和前面几章的关系

常量是写死的值,变量是「跑的时候才知道值」的容器。 存储过程、脚本、动态 SQL 全靠它。 教材 6.3 分三小节:局部变量、全局变量、作用域。 第三节最实用——「变量未声明」这个报错,八成就是作用域问题。 演示里没法真跑 T-SQL 的 DECLARE(本沙箱没有 SQL Server),所以用 Python 变量复刻语义, 再把「GO 切断作用域」这件事用两段批处理演清楚。

一、声明、赋值、使用

教材的写法是这样三段:

DECLARE @name NVARCHAR(20), @age INT
SET @name = '张三'
SELECT @age = 18
SELECT @name AS 姓名, @age AS 年龄

DECLARE 一次可以声明多个变量,用逗号隔开,每个都要写类型。这一点跟 C 语言一样, 不能写成 DECLARE @a, @b INT。 SET 和 SELECT 都能赋值,但行为不同。 SET 一次赋一个值,语义清楚; SELECT 可以一次给多个变量赋值,还能从表里查出来赋——代价是它有坑。 那个坑是:SELECT @x = 某列 FROM 表 如果查出多行,它只取最后一行的值,而且不报错。 你以为取的是第一行,实际取的是最后一行——这种错查起来极费时间,因为不报错。 所以建议:从一个值赋值用 SET,从表里赋值用 SELECT TOP 1 ... ORDER BY。 把「取哪一行」写清楚,比赌它取哪一行安全。

二、作用域:GO 是分界线

这是本节最该记住的一条。T-SQL 的变量只活在「当前批处理」里,而 GO 就是批处理的分界。

DECLARE @x INT
SET @x = 100
SELECT @x        -- 输出 100
GO
SELECT @x        -- 报错:必须声明标量变量 @x

为什么会这样?因为 GO 不是 SQL 语句,是客户端的分隔符。 它把上面的代码切成一批, 单独发给服务器编译一次。每一批是独立编译的,批 1 的变量批 2 根本看不见。 这个坑在长脚本里特别常见。 一个几百行的脚本,开头 DECLARE 了一堆变量, 中间某处夹了个 GO(可能是复制粘贴带进来的),后面全报「未声明」。 排查方法:看报错的行号,往上找最近的一个 GO。 如果 DECLARE 在那个 GO 之前, 就是这个问题——把 DECLARE 挪到 GO 后面,或者把 GO 删掉。 顺带说一句:GO 也不认变量。 GO 10 表示「重复执行 10 次」, 但写成 GO @n 是不行的——因为客户端在发之前就要读这个数字,那时候变量还不存在。

三、全局变量:只能读

变量 含义 典型用途
@@VERSION 服务器版本字符串 排查兼容性
@@SERVERNAME 实例名 确认连的是哪台
@@ROWCOUNT 上条语句影响的行数 判断有没有改到
@@TRANCOUNT 当前嵌套事务层数 判断是否在事务里
@@ERROR 上条语句的错误号 老式错误处理

这些以 @@ 开头的是系统函数,只读。 想给它们赋值会报错—— 而且报的错很奇怪(「名称不是可识别的选项」),第一次见根本想不到是这个原因。 @@ROWCOUNT 的陷阱再强调一次:任何一条语句执行后它都会重置。 所以「更新完看影响了多少行」这件事,必须紧接着做,中间不能插任何语句。 @@TRANCOUNT 在调试事务的时候很有用。 它的值是「当前嵌套了几层事务」——0 表示不在事务里, 大于 0 表示在。看到它不为 0 就提交不了,这是个常见的坑。 @@ERROR 现在基本不用了。 SQL Server 2005 之后有 TRY...CATCH, 比「每条语句后面查一次 @@ERROR」清楚得多。老代码里还能看到,看懂就行。

四、变量与参数的区别

这两个容易混,但它们在存储过程里的位置完全不同。

对比 变量 参数
声明方式 DECLARE @x INT 写在过程名后面的括号里
谁给值 过程内部自己赋 调用的人传进来
作用范围 当前批处理 整个过程
能带默认值吗 不适用 能,@x INT = 0

参数是「外面传进来的」,变量是「里面自己用的」。 存储过程内部, 参数可以当变量用(能改它的值),但反过来不行。 输出参数(OUTPUT)是另一回事。 它让过程能把值传回给调用者——声明时加 OUTPUT,调用时也要加。 忘了加任何一个,拿到的都是 NULL。 这一节只提一句,第 12 章讲存储过程时会展开。 现在只需要分清:看到 @ 开头的东西, 先判断它是变量还是参数——看它在 CREATE PROCEDURE 的括号里还是外面。

五、动手 Demo:变量与 GO 切断作用域

这一段贴教材的声明赋值原码(本沙箱不执行),再用 Python 变量复刻「声明—赋值—读取」, 然后把两段批处理并排摆出来,看 GO 之前声明的变量在 GO 之后会怎样。

# 复刻「声明 -> 赋值 -> 读取」这个流程
name, age = u"张三", None
age = 18
print(u"name=%s age=%s" % (name, age))

# 两段批处理:GO 是分界线
batch1 = [u"DECLARE @x INT", u"SET @x = 100", u"SELECT @x"]
batch2 = [u"SELECT @x"]
for label, lines, expect in (
        (u"批 1", batch1, u"输出 100"),
        (u"批 2", batch2, u"报错:必须声明标量变量 @x")):
    print(label)
    for ln in lines:
        print(u"  " + ln)
    print(u"  结果:" + expect)

实跑输出:

--------------------------------------------------------------------
变量与作用域
--------------------------------------------------------------------


  ① 教材原码:声明、赋值、使用
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | DECLARE @name NVARCHAR(20), @age INT
  | SET @name = '张三'
  | SELECT @age = 18
  | SELECT @name AS 姓名, @age AS 年龄

  DECLARE 声明,SET 或 SELECT 赋值,两者都能赋值。
  区别是:SET 一次赋一个值,SELECT 可以一次给多个变量赋值,而且能从表里查出来赋。
  但 SELECT 赋值有个坑:如果查询返回多行,它只取最后一行的值,不报错。

  ② 用 Python 变量把它们复刻出来
  --------------------------------------------------------------------
  name = 张三   age = 18
  这批代码演的是「声明—赋值—读取」这个流程,不是 T-SQL 语法本身。
  T-SQL 的变量只在「当前批处理」里活着——GO 一分批,变量就没了。

  ③ 作用域:GO 是分界线
  --------------------------------------------------------------------
  批 1:
    DECLARE @x INT
    SET @x = 100
    SELECT @x
    结果:输出 100
  批 2:
    SELECT @x
    结果:报错:必须声明标量变量 @x
  GO 不是 SQL 语句,是客户端的分隔符——它把上面的代码切成一批一批发给服务器。
  每批是独立的一次编译,所以批 1 里声明的变量,批 2 根本看不见。
  这个坑很常见:一个长脚本里在开头 DECLARE,中间夹了个 GO,后面就全报「未声明」。

  ④ 全局变量:只能读,不能写
  --------------------------------------------------------------------
| 变量 | 含义 |
|---|---|
| @@VERSION | 服务器版本字符串 |
| @@SERVERNAME | 实例名 |
| @@ROWCOUNT | 上一条语句影响的行数 |
| @@TRANCOUNT | 当前嵌套的事务层数 |

  这些以 @@ 开头的是系统函数,只读。想给它们赋值会报错。
  注意 @@ROWCOUNT 用完就变——任何一条语句执行后它都会重置,
  所以要立刻把它存到自己的变量里,否则下一句一跑就没了。

第一段复刻的是流程,不是语法。 Python 没有 DECLARE, 也没有 @ 前缀——它演的是「先有个空容器,再往里放值,再读出来」这件事。 第二段才是这一节的重点。 两段批处理摆在一起看:批 1 声明并用了 @x, 批 2 直接读 @x 就报错——因为 GO 把它们切成了两次独立编译。 这个报错在实际工作里非常常见,而且很迷惑。 你明明看到上面声明了, 报错却说「未声明」——原因就是中间那个 GO。 排查口诀:报「未声明」,往上找最近的 GO。 如果 DECLARE 在它前面,那就是这个问题。 把 DECLARE 挪到 GO 后面,或者把 GO 删掉。

30 秒实验

  1. 写一段 DECLARE @i INT = 1; WHILE @i <= 3 BEGIN PRINT @i; SET @i = @i + 1 END, 注意 DECLARE @i INT = 1 这种写法是 2008 之后才支持的,老版本要分两句。
  2. 在中间插一个 GO,看它报什么错。这就是作用域被切断的样子。
  3. 想一想:为什么 @@ROWCOUNT 要立刻存下来? 因为任何语句执行后它都会重置——包括你用来读它的那条 SELECT。 所以读完就没了。

常见误区

  1. 「SELECT @x = 列 FROM 表 取的是第一行」。取的是最后一行,而且不报错。 要取第一行必须写 SELECT TOP 1 ... ORDER BY。这种错查起来极费时间,因为它不报错。
  2. 「变量在整个脚本里都有效」。只在当前批处理里有效。 GO 一分批就断——这是「变量未声明」最常见的成因。
  3. 「@@ROWCOUNT 可以随时查」。任何语句都会重置它。 要判断「刚才那条更新影响了几行」,必须紧接在做完之后,中间不能插 PRINT、 不能插 SELECT。
自测三问(点开对答案) 1. `SET @x = 值` 和 `SELECT @x = 值` 有什么区别? —— **`SET` 一次赋一个值,语义清楚;`SELECT` 可以一次给多个变量赋值,也能从表里查出来赋。** 但 `SELECT` 从表里赋值时如果返回多行,只取最后一行而且不报错——**这是它的坑。** 2. 为什么 `GO` 之后变量就用不了了? —— **因为 `GO` 不是 SQL 语句,是客户端的分隔符。** 它把代码切成一批一批单独编译, 每一批是独立的编译单元——**批 1 声明的变量,批 2 根本不知道它存在。** 3. `@@TRANCOUNT` 是 1 表示什么? —— **表示当前在事务里,而且是第一层。** 它的值是嵌套事务的层数,0 表示不在事务里。 **看到它不为 0 却「提交不了」,就是有嵌套事务没提交完。**

下一篇 6.4 讲语句块和运算符。 注意教材这一节的标题叫「流程控制语句」,但内容只讲了 BEGIN...END 和运算符——真正的 IF、 WHILE 在下一节。

相关笔记