【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 -