原文地址:https://yq.aliyun.com/articles/69187
摘要: # 摘要 通過前面的三篇系列文章,我們對臨時表和表變量的概念、對比和認知誤區已經有了非常全面的認識。其實,我們的終極目的,還是今天要討論的話題,即當我們面對具體的業務場景的時候,該選擇臨時表還是表變量? # 幾種典型場景 以下是幾種典型的場景,讓我們看看到底該作何選擇,以及做出最終選擇的具體原因和考量。 ## 存儲過程嵌套 在SQL Server中,使用存儲過程的好處顯而易見,往往會節約
摘要
通過前面的三篇系列文章,我們對臨時表和表變量的概念、對比和認知誤區已經有了非常全面的認識。其實,我們的終極目的,還是今天要討論的話題,即當我們面對具體的業務場景的時候,該選擇臨時表還是表變量?
幾種典型場景
以下是幾種典型的場景,讓我們看看到底該作何選擇,以及做出最終選擇的具體原因和考量。
存儲過程嵌套
在SQL Server中,使用存儲過程的好處顯而易見,往往會節約存儲過程執行計划編譯時間,提高查詢語句的執行效率。有時候,我們在構建存儲過程多層次嵌套場景中,會有內層存儲過程需要臨時使用外層存儲過程的“暫存”數據。在SQL Server暫存臨時數據的方法可以使用臨時表或者表變量,但是在這種場景中,僅臨時表適合。比如,下面的例子:
-- Scenario 1: Nest Store Procedure USE tempdb GO IF OBJECT_ID('tempdb..#UP_Inner', 'P') IS NOT NULL DROP PROC #UP_Inner GO CREATE PROC #UP_Inner AS BEGIN SET NOCOUNT ON UPDATE A SET Comment = 'INNER' FROM #temp AS A WHERE RowID = 1; END GO IF OBJECT_ID('tempdb..#UP_Outer', 'P') IS NOT NULL DROP PROC #UP_Outer GO CREATE PROC #UP_Outer AS BEGIN SET NOCOUNT ON IF OBJECT_ID('tempdb..#temp', 'U') IS NOT NULL DROP PROC #temp CREATE TABLE #temp( RowID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY ,Comment VARCHAR(100) NULL) INSERT INTO #temp(Comment) VALUES(''),('OUTER') -- check the data before call inner SP SELECT * FROM #temp -- call the inner store procedure EXEC #UP_Inner -- check the data after call inner SP SELECT * FROM #temp END GO -- call the outer SP EXEC #UP_Outer -- END Scenario 1: Nest Store Procedure
我們在內層存儲過程將臨時表#temp字段Comment更新為INNER;外層存儲過程在調用內層存儲過程前后分別查詢臨時表的數據。從這個結果來看,內層存儲過程完全可以使用外層存儲過程創建的臨時表。這種場景無法使用表變量,因為內層存儲過程會因為表變量沒有定義而報錯。
服務啟動自動執行存儲過程
有時候,我們需要在SQL Server Service啟動完畢后,立馬自動執行某個存儲過程以獲取某些重要的數據信息。比如:我們想知道SQL Server服務啟動后,到底有哪些用戶連接到了SQL Server服務器。我們可以選擇使用全局臨時表來暫存用戶信息,而且其他進程也可以查看相應的數據信息。方法如下:
-- Scenario 2: auto execution SP when startup USE master GO EXEC sys.sp_configure 'show advanced options', 1 GO EXEC sys.sp_configure 'scan for startup procs', 1 GO RECONFIGURE WITH OVERRIDE GO -- create sp to call when sql server serivce startup CREATE PROC dbo.UP_GetLoginUserWhenStartup AS BEGIN SET NOCOUNT ON IF OBJECT_ID('tempdb..##temp', 'U') IS NOT NULL DROP TABLE ##temp CREATE TABLE ##temp( RowId INT IDENTITY(1, 1) NOT NULL, LoginName varchar(200) NOT NULL ) INSERT INTO ##temp SELECT DISTINCT loginame FROM sys.sysprocesses END GO -- registe to auto execution when startup EXEC sys.sp_procoption 'UP_GetLoginUserWhenStartup', 'startup', 'on' GO -- END Scenario 2: auto execution SP when startup
重啟SQL Server Service,然后新開一個連接執行下面的語句,結果如下:
SQL Server Service重啟完畢后,系統會自動執行Master數據庫下的存儲過程dbo.UP_GetLoginUserWhenStartup以獲取到哪些用戶連接到SQL Server。這個過程和Linux開機自動執行自定義腳本或者服務非常類似。由於其他進程需要查看抓取到的信息,在此使用全局臨時表而不是表變量。
暫存大量數據
在很多場景我們需要暫存大量數據或者根本無法預估需要暫存的數據量。這里需要首先明確的一個問題是,到底暫存多大的數據量算大量?根據上一篇文章SQL Server 臨時表和表變量系列之認知誤區篇中的章節“表變量僅駐留在內存中”的內容,我們可以認為需要暫存的數據量大小接近或者超過SQL Server最大可以使用內存一半的時候,這個數據量就是大量數據。比如:當SQL Server的Max Server Memory設置為1GB,需要暫存的數據量接近或者超過512MB時,512MB就是大量數據;但是,當SQL Server Max Server Memory為10GB甚至更高,需要暫存512MB時,這個數據量又不算是大量數據。在大量數據需要暫存時,無論使用臨時表或者表變量,SQL Server系統最終會將數據存在Tempdb的數據文件磁盤上。所以這個時候,請選擇使用臨時表來暫存數據,最好是能夠根據業務場景為臨時表創建合適的索引,以提高后續臨時表查詢語句的執行效率。
需要支持用戶事務
有時候用戶業務場景需要暫存的數據結構支持用戶事務,在這種場景下,我們應該選擇臨時表。根據我們之前的文章SQL Server 臨時表和變量系列之對比篇的“對事務支持”部分,我們知道,表變量不支持用戶事務回滾,而臨時表對用戶事務的支持和正式表沒有任何差異。所以,在這種場景下,我們需要選擇臨時表作為暫存數據結構。
表值函數的返回值
表值函數的返回值為表變量,這個場景中,是無法使用臨時表來替換的。比如:我想要找出當前數據庫下表名稱中含有某個特定字符串的所有表信息,用表值函數來實現的方式如下:
-- Scenario: function returned temp variables USE AdventureWorks2014 GO IF OBJECT_ID('dbo.UTF_FindTables', 'TF') IS NOT NULL DROP FUNCTION dbo.UTF_FindTables GO CREATE FUNCTION dbo.UTF_FindTables( @partner sysname ) RETURNS @Tables TABLE( RowID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY ,tb_Object_ID BIGINT NULL ,database_name SYSNAME NULL ,schema_name SYSNAME NULL ,object_Name SYSNAME NULL ) AS BEGIN INSERT INTO @Tables SELECT object_id, DB_NAME(), SCHEMA_NAME(schema_id), name FROM sys.tables WHERE name like '%' + @partner + '%'; RETURN END GO -- Calling example SELECT * FROM dbo.UTF_FindTables('Person') -- END Scenario
查找當前數據庫下表名字中含有Person關鍵字的表詳情。由於表值函數返回值僅支持表變量,所以這種場景中是無法使用臨時表的。
高並發場景選擇表變量
在SQL Server數據庫高並發場景中,請慎重選擇臨時表的使用,建議使用表變量。對於這個場景的測試,我們會使用到SQLTest這個測試工具,關於這個工具的使用請參照我之前的文章SQLTest系列之INSERT語句測試。
首先,我們創建兩個測試存儲過程,它們的邏輯一模一樣,唯一不同的是一個使用表變量,一個使用臨時表。
USE AdventureWorks2014 GO IF OBJECT_ID('dbo.UP_TableVariables', 'P') IS NOT NULL DROP PROC dbo.UP_TableVariables GO CREATE PROC dbo.UP_TableVariables AS BEGIN SET NOCOUNT ON DECLARE @t1 TABLE( c1 INT, c2 INT, c3 CHAR(2000) ) ;WITH DATA AS( SELECT * FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9)) AS T(C) ) INSERT INTO @t1 SELECT a.C, b.C, REPLICATE('A', 2000) FROM DATA as a, data as b END GO IF OBJECT_ID('dbo.UP_TempTable', 'P') IS NOT NULL DROP PROC dbo.UP_TempTable GO CREATE PROC dbo.UP_TempTable AS BEGIN SET NOCOUNT ON CREATE TABLE #t1 ( c1 INT, c2 INT, c3 CHAR(2000) ) ;WITH DATA AS( SELECT * FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9)) AS T(C) ) INSERT INTO #t1 SELECT a.C, b.C, REPLICATE('A', 2000) FROM DATA as a, data as b END GO
接下來,我們使用兩個Workload,每個Workload開啟20個進程。Workload 1執行EXEC dbo.UP_TableVariables 100次;workload 2執行EXEC dbo.UP_TempTable 100次。測試代碼如下:
USE AdventureWorks2014 SET NOCOUNT ON DECLARE @i INT ; SET @i = 1 ; WHILE @i <= 100 BEGIN EXEC dbo.UP_TableVariables --EXEC dbo.UP_TempTable SET @i = @i + 1 END GO
測試時間為120秒,測試的結果如下:
Workload 1:表變量在高並發場景的性能表現為:完成迭代340次,每一次數據庫平均時間消耗為6.835秒。
Workload 2:臨時表在高並發場景下的性能表現為:完成迭代280次,每一次數據庫平均時間消耗為8.321秒。
從我的測試機測試的結果來看,表變量在20個並發場景,迭代次數也有20%多的增加,相應的平均時間消耗比臨時表有20%多的性能提升。基於這個測試結果來看,在高並發使用場景下,選擇表變量而不是臨時表。


