1. 程式人生 > >【SQL】小心在迴圈中宣告變數——淺析SQL變數作用域

【SQL】小心在迴圈中宣告變數——淺析SQL變數作用域

本文適用:T-SQL(SQL Server)

先看這個語句:

DECLARE @i INT = 0
WHILE @i < 3 --跑3圈
BEGIN
    --每圈都定義一個表變數,並插入一行
    DECLARE @t TABLE(Col INT PRIMARY KEY) --主鍵唯一約束
    INSERT @t VALUES (1)

    SET @i += 1
END

如果你認為這個語句跑起來沒問題,那你值得看下去,會避免以後踩到【SQL變數作用域】的坑。

事實上這個語句會報2次“違反了PRIMARY KEY約束…”,原因是@t這個表變數,並不是在每一圈都重新宣告一個新的,而是宣告1次後就一直沿用,由於該表具有主鍵約束,所以之後的兩圈在插入的時候,由於已經存在相同主鍵,於是報上述錯誤。

換成普通變數也一樣:

DECLARE @i INT = 0
WHILE @i < 3 --跑3圈
BEGIN
    --同樣,該變數也只會宣告1次,之後沿用
    DECLARE @s VARCHAR(20)

    IF @s IS NULL --所以第1圈會進入該分支
        SET @s = 's'
    ELSE          --之後的圈則進入該分支
        SET @s += 's'
    
    PRINT @s
    
    SET @i += 1
END

--執行結果:
s
ss
sss

所以到這裡能得出一個結論:

迴圈中的變數只會宣告一次,並在之後一直沿用。

理解這一點很重要,因為這與C#等編譯語言非常不同,C#中每一圈宣告的變數都相當於重新建一個,與上一圈的毫無關係,但在sql中不能這麼思考。

 

嘗試把上面的語句小改一下:

DECLARE @i INT = 0
WHILE @i < 3 --跑3圈
BEGIN
    DECLARE @s VARCHAR(20) = 's' --宣告並賦值
    SET @s += 's'
    
    PRINT @s
    
    SET @i += 1
END

這次得到的結果會是3個ss,看起來是@s在每一圈得到了重建,那這似乎與上面的結論有悖,不是隻會宣告1次嗎?其實並沒有矛盾,而是【declare @s xxx = 's'】相當於【declare @s xxx】+【set @s = 's'】倆語句,宣告的確只有1次,但稍後的賦值卻是每圈都在進行,相當於每圈一開始都把@s重置為's',所以是這個結果。這也提醒:見到declare @x xxx = xxx時,要看成兩個動作

 

其實這個問題本質上是一個變數作用域問題,只不過SQL中的變數作用域,與C#等語言按語句塊劃分不一樣,SQL的變數作用域是【批】,這一點在MSDN中有說。比如下面的語句:

IF 1 = 2
    DECLARE @s VARCHAR(20)

SELECT @s

按說declare @s並不會得到執行,@s並沒有宣告,但事實上這個語句一切正常,不會報錯。原因就在於宣告語句比較特殊,它並不依賴位置,系統“見到”就算數,所以不管變數在多深的語句塊中宣告,它在本批接下來的語句中都是有效的。印象中某種SQL的寫法是宣告在一個區,邏輯在一個區,既然你t-sql的宣告具有“提升”這種特點,我認為做成那種比較好,而不是混在邏輯語句中搞特殊。

回到開頭的問題,現在我們清楚,雖然變數在迴圈中宣告,但它並不會被多次執行,甚至不是在第1圈的時候執行,而是在某個時機由系統將所有宣告統一執行,大概類似C#的靜態欄位,不管定義在哪裡,CLR會確保在使用該類前完成初始化。

 

至於什麼叫一【批】SQL,我沒有找到很正式的定義,根據所學,我的理解是:沒GO就是一批;有GO的話,GO之間算一批;exec、sp_executesql算一批;ssms中選中執行的部分算一批(前提是選中部分不含上述劃分點)。如有錯漏還請指正,感謝。

 

- EOF -