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 秒实验
- 写一段
DECLARE @i INT = 1; WHILE @i <= 3 BEGIN PRINT @i; SET @i = @i + 1 END, 注意DECLARE @i INT = 1这种写法是 2008 之后才支持的,老版本要分两句。 - 在中间插一个
GO,看它报什么错。这就是作用域被切断的样子。 - 想一想:为什么
@@ROWCOUNT要立刻存下来? 因为任何语句执行后它都会重置——包括你用来读它的那条SELECT。 所以读完就没了。
常见误区
- 「
SELECT @x = 列 FROM 表取的是第一行」。取的是最后一行,而且不报错。 要取第一行必须写SELECT TOP 1 ... ORDER BY。这种错查起来极费时间,因为它不报错。 - 「变量在整个脚本里都有效」。只在当前批处理里有效。
GO一分批就断——这是「变量未声明」最常见的成因。 - 「
@@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 在下一节。
- ☐ (本篇完,下一篇 6.4 流程控制语句)