前言
之前我們已經討論過動態SQL查詢呢?這里為何再來探討一番呢?因為其中還是存在一定問題,如標題所言,很多面試題也好或者有些博客也好都在說在執行動態SQL查詢時sp_executesql的性能比exec好,但是事實真是如此?下面我們來一探究竟。
探討sp_executesql和exec執行動態SQL查詢性能
首先我們創建如下測試表。
CREATE TABLE dbo.TestDynamicSQL ( Col1 INT PRIMARY KEY , Col2 SMALLINT NOT NULL , CreatedTime DATETIME DEFAULT GETDATE() , OtherValue CHAR(10) DEFAULT 'Jeffcky' ) GO
接着再來插入數據,如下:
INSERT dbo.TestDynamicSQL ( Col1, Col2 ) SELECT number + 1 , number FROM master..spt_values WHERE type = 'P' ORDER BY number
最終查詢為如下測試數據:
接下來我們執行如下兩個SQL查詢語句,執行4次。
SELECT * FROM dbo.TestDynamicSQL WHERE Col2 = 3 AND Col1 = 4 GO SELECT * FROM dbo.TestDynamicSQL WHERE Col2 = 4 AND Col1 = 5 GO
緊接着我們通過如下SQL語句來查詢緩存計划。
SELECT q.text , cp.usecounts , cp.objtype , p.* , q.* , cp.plan_handle FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) p CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS q WHERE cp.cacheobjtype = 'Compiled Plan' AND q.text LIKE '%dbo.TestDynamicSQL%' AND q.text NOT LIKE '%sys.dm_exec_cached_plans %'
由上圖可知,我們看到存在兩個查詢計划且每個執行了4次,也就是說每一次查詢都會重新生成一個新的計划。清除查詢計划緩存,通過如下命令:
DBCC FREEPROCCACHE
我們繼續往下走,我們接下來通過EXEC來執行動態SQL查詢,如下,執行查詢完畢后再來看看查詢計划次數:
DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 11 , @Col1 = 12 DECLARE @SQL VARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC (@SQL) GO DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 12 , @Col1 = 13 DECLARE @SQL VARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC (@SQL) GO
這個就不做過多解釋,我們依然要清除查詢計划緩存,我們再利用sp_executesql來查詢,如下:
DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 23 , @Col1 = 24 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC sp_executesql @SQL Go DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 22 , @Col1 = 23 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC sp_executesql @SQL GO
對比exec執行動態SQL查詢得到的結果是一模一樣,正如我所演示的,我們有兩個計划,每個執行次數為4。不是說sp_executesql執行動態SQL查詢會重用計划緩存么,這是因為我們沒有正確使用sp_executesql所以導致SQL引擎無法重用計划。
當參數值改變為語句是唯一變化時,可以使用sp_executesql代替存儲過程多次執行Transact-SQL語句。 因為Transact-SQL語句本身保持不變,只有參數值發生變化,因此SQL Server查詢優化器可能會重用為第一次執行生成的執行計划。
以下是正確參數化的查詢方式,我們在字符串里面有一些變量,在執行的時候,我們通過其他變量傳遞值給它。
DECLARE @Col2 SMALLINT , @Col1 INT SELECT @Col2 = 3 , @Col1 = 4 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = @InnerCol2 and Col1 = @InnerCol1' DECLARE @ParmDefinition NVARCHAR(500) SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int' EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1 GO DECLARE @Col2 SMALLINT , @Col1 INT SELECT @Col2 = 3 , @Col1 = 4 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = @InnerCol2 and Col1 = @InnerCol1' DECLARE @ParmDefinition NVARCHAR(500) SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int' EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1 GO
我們看到只有一個計數為8的計划,而不是像我們上述那樣運行查詢。 我們也可以只需要聲明一次,然后我們只需要在執行之前更改參數的值,如下:
DECLARE @Col2 SMALLINT , @Col1 INT SELECT @Col2 = 3 , @Col1 = 4 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = @InnerCol2 and Col1 = @InnerCol1' DECLARE @ParmDefinition NVARCHAR(500) SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int' EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1 --change param values and run the same query SELECT @Col2 = 2 , @Col1 = 3 EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1
最終查詢計划緩存次數和上述正確方式一致。正確使用sp_executesql對於性能非常有利,而且使用sp_executesql還可以為我們提供一些EXEC無法實現的功能。比如如何得到一個表中的行數? 利用EXEC我們需要使用一個臨時表和填充,而用sp_executesql我們只需要使用一個輸出變量。
SET STATISTICS IO ON SET STATISTICS TIME ON --EXEC (SQL) DECLARE @Totalcount INT , @SQL NVARCHAR(100) CREATE TABLE #temp (Totalcount INT ) SELECT @SQL = 'Insert into #temp Select Count(*) from dbo.TestDynamicSQL' EXEC( @SQL) SELECT @Totalcount = Totalcount FROM #temp SELECT @Totalcount AS Totalcount DROP TABLE #temp GO --sp_executesql DECLARE @TableCount INT, @SQL NVARCHAR(100) SELECT @SQL = N'SELECT @InnerTableCount = COUNT(*) FROM dbo.TestDynamicSQL' EXEC SP_EXECUTESQL @SQL, N'@InnerTableCount INT OUTPUT', @TableCount OUTPUT SELECT @TableCount GO
當然除了EXEC無法實現的功能外,最重要的一點則是SP_EXECUTESQL能夠防止SQL注入問題。
總結
執行SQL動態查詢SP_EXECUTESQL比EXEC性能更好,使得存儲過程能夠被重用,但是存儲過程能夠被重用的前提則是正確使用參數,使用參數化查詢,否則SP_EXECUTESQL將不會提供任何性能益處。