前言
之前有園友一直關注着我快點出SQL Server性能優化系列,博主我也對性能優化系列也有點小期待,本來打算利用周末寫死鎖以及避免死鎖系列的接着進入SQL Server優化系列,但是在工作中長時間都是利用EF來操作SQL,不免對寫SQL語句有些生疏,在某些場景下還是只能利用底層的SQL語句或者寫存儲過程來實現,很久沒寫存儲過程都忘記怎么寫了,所以本節穿插動態SQL查詢的文章,別着急,博主說過不會爛尾,博主再忙也會抽空將整個SQL Server系列梳理完畢,那樣的話,無論對初級還是中級者都可以從中受益匪淺,至少我是這么認為,呵呵。
動態SQL語句查詢
前前篇我們簡短敘述了利用EXEC和EXECUTE來進行動態SQL語句查詢,並未深入去講解,借博主工作中重新回到寫原生SQL語句的機會,我們再來回顧下動態SQL語句查詢。既然是動態SQL查詢,說明在某些場景下利用硬編碼SQL語句查詢的方式是不可行的,比如查詢條件的不固定,這是最常見的情景,那么動態SQL語句查詢有哪幾種方式呢?萬變不離其宗,只有以下三種方式,請往下看。
參數化SQL語句動態查詢
參數化SQL查詢是動態SQL查詢中最簡單的一種,因為我們只需要傳遞參數即可,查詢條件是固定的,我們一起來溫故而知新。
USE AdventureWorks2012 DECLARE @AccountNumber AS VARCHAR(200) SET @AccountNumber = 'AW00000002' SELECT StoreID, CustomerID, ModifiedDate, PersonID FROM Sales.Customer WHERE AccountNumber = @AccountNumber
EXEC動態SQL語句動態查詢
這個相對來說比上一個要略微復雜一點,對於復雜的SQL語句拼接,我們來看看。
USE TSQL2012 DECLARE @shipcity AS VARCHAR(50) DECLARE @sqlCommand AS VARCHAR(500) DECLARE @columnList AS VARCHAR(200) SET @shipcity = 'Lyon' SET @columnList = 'orderid, custid, orderdate, shipname, shipaddress, shipcity' SET @sqlCommand = 'SELECT '+ @columnList + ' FROM Sales.Orders WHERE shipcity = '+ @shipcity EXEC(@sqlCommand)
居然出錯了,讓人始料未及,當設置參數值時我們應該將 SET @shipcity = 'Lyon' 進行如下修改:
SET @shipcity = '''Lyon'''
sp_executesql動態SQL語句查詢(推薦)
USE TSQL2012 DECLARE @shipcity AS VARCHAR(50) DECLARE @sqlCommand AS VARCHAR(500) DECLARE @columnList AS VARCHAR(200) SET @shipcity = 'Lyon' SET @columnList = 'orderid, custid, orderdate, shipname, shipaddress, shipcity' SET @sqlCommand = 'SELECT '+ @columnList + ' FROM Sales.Orders WHERE shipcity = @shipcity' EXECUTE sp_executesql @sqlCommand, N'@shipcity VARCHAR(50)', @shipcity = @shipcity
之前從未遇到過這種情況,查此錯誤居然發現要:sp_executesql執行的SQL必須定義為NVARCHAR類型即必須定義為UNICODE,這里算是學習了。
對於利用sp_executesql來執行動態sql語句查詢的方式作為推薦最主要是因為其在查詢執行計划中,無論其變量值是否改變查詢計划都會進行重用,當然還有其他好處,比如利用EXEC來執行查詢,此時進行拼接非常容易出錯。同時在寫動態SQL時個人一般推崇將查詢的列作為一個列表進行定義,對於參數也進行定義,這樣可維護性強不至於看起來亂糟糟的利於后續排查問題。
講到這里是不是敘述完畢了呢,那就太沒意思了,相信看過博主所寫的內容一般都是由淺入深,下面我們繼續往下看。
一直講的動態SQL語句查詢,你難道就沒有懷疑過僅僅只能進行查詢,難道不能創建表或者創建視圖么,答案當然是可以的。我們簡單看下創建表。
Declare @SQL VarChar(1000) DECLARE @TableName AS VARCHAR(10) SET @TableName = 'Test' SELECT @SQL = 'Create Table ' + @TableName + '(' SELECT @SQL = @SQL + 'ID int NOT NULL Primary Key, FieldName VarChar(10))' Exec (@SQL)
利用EXEC或者sp_executesql居然還能創建表,還能最高級一點么,答案當然是可以,請繼續往下看。
當我們在不是當前打開會話中的數據庫中去創建另外數據庫的視圖時可行不可行呢?
Create View AdventureWorks2012.dbo.Auths AS
(SELECT
StoreID, CustomerID, ModifiedDate, PersonID
FROM Sales.Customerl
)
此時創建視圖你會發現不能正常創建視圖,說明創建視圖必須是在當前會話指定的數據庫中才可以。
但是利用sp_executesql就能解決跨數據庫創建視圖的問題,如下:
DECLARE @SQL NVarChar(1000) SELECT @SQL = 'Create View Auths AS (SELECT CustomerID, ModifiedDate FROM Sales.Customer)' EXECUTE AdventureWorks2012.dbo.sp_executesql @sql
此時你會發現利用sp_executesql不僅可以重用查詢執行計划並且可以跨數據庫創建視圖,那么它難道就沒有限制么,答案當然是有的,請繼續往下看。
存儲過程執行動態SQL語句查詢
當在存儲過程中執行動態SQL語句查詢時,此時動態SQL語句是在當前用戶的權限下進行而不是調用存儲過程的用戶的權限,換句話說如果我們對存儲過程上的表沒有權限,那么運行存儲過程將出現問題。還是不理解是什么意思么,也就是說存儲過程運行是有其作用域的。我們舉一個例子,我們可以利用如下語句來設置查詢的行數。
SET ROWCOUNT 3
所以接下來我們利用上述語句來查詢返回的行數。
EXEC('SET ROWCOUNT 3') SELECT * FROM Sales.Customers EXEC('SET ROWCOUNT 0')
從上我們可以發現我們設置限制返回的行數根本不起作用,這是因為設置返回的行數已經超出了查詢的作用域。所以我們必須將其設置限制行數放到EXEC中,如下:
EXEC('SET ROWCOUNT 3 SELECT * FROM Sales.Customers SET ROWCOUNT 0')
WHERE 1 = 1問題
對於不確定多條件篩選利用WHERE 1 = 1是否會帶來性能問題,但是除此之外似乎沒有什么好的辦法比利用WHERE 1 = 1來減少條件的判斷從而加長SQL語句,可能會導致性能的下降。下面我們來看一個簡單的例子
USE AdventureWorks2012 GO IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Sales].[GetSalesOrders]') AND type in (N'P', N'PC')) DROP PROCEDURE [Sales].[GetSalesOrders] GO CREATE PROCEDURE [Sales].[GetSalesOrders] ( @CustomerID INT = NULL, @CreditCardID INT = NULL) AS SET NOCOUNT ON; DECLARE @SQL NVARCHAR(4000); DECLARE @ParameterDefinition NVARCHAR(4000); SELECT @ParameterDefinition = ' @CustomerParameter INT, @CreditCardIDParameter INT '; SELECT @SQL = N' SELECT [SalesOrderID], [OrderDate], [Status], [CustomerID], [CreditCardID] FROM [Sales].[SalesOrderHeader] WHERE 1 = 1 '; IF @CustomerID IS NOT NULL SELECT @SQL = @SQL + N' AND CustomerID = @CustomerParameter '; IF @CreditCardID IS NOT NULL SELECT @SQL = @SQL + N' AND CreditCardID = @CreditCardIDParameter '; EXECUTE sp_executesql @SQL, @ParameterDefinition, @CustomerParameter = @CustomerID, @CreditCardIDParameter = @CreditCardID; GO SET NOCOUNT OFF;
再復雜也不過就是多表查詢和多條件篩選罷了,還是比較簡單。得出如下結果。
上述利用存儲過程創建動態SQL語句沒什么毛病。
那么問題來了,要是如果我們想調試創建過程生成的SQL語句是否有沒有錯誤,我們此時該如何做呢?為了利於調試我們可以將上述修改如下,添加一個debug標識來打印SQL。
USE AdventureWorks2012 GO IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Sales].[GetSalesOrders]') AND type in (N'P', N'PC')) DROP PROCEDURE [Sales].[GetSalesOrders] GO CREATE PROCEDURE [Sales].[GetSalesOrders] ( @CustomerID INT = NULL, @CreditCardID INT = NULL, @debug bit = 0) AS SET NOCOUNT ON; DECLARE @SQL NVARCHAR(4000); DECLARE @ParameterDefinition NVARCHAR(4000); SELECT @ParameterDefinition = ' @CustomerParameter INT, @CreditCardIDParameter INT '; SELECT @SQL = N' SELECT [SalesOrderID], [OrderDate], [Status], [CustomerID], [CreditCardID] FROM [Sales].[SalesOrderHeader] WHERE 1 = 1 '; IF @CustomerID IS NOT NULL SELECT @SQL = @SQL + N' AND CustomerID = @CustomerParameter '; IF @CreditCardID IS NOT NULL SELECT @SQL = @SQL + N' AND CreditCardID = @CreditCardIDParameter '; IF @debug = 1 PRINT @SQL EXECUTE sp_executesql @SQL, @ParameterDefinition, @CustomerParameter = @CustomerID, @CreditCardIDParameter = @CreditCardID; GO EXEC [Sales].[GetSalesOrders] @debug = 1, @CustomerID = 29565 SET NOCOUNT OFF;
此時我們調試發現SQL語句沒有寫錯並且生成的結果如下:
同時我們在消息中可以看到生成的SQL語句如下,這樣能夠准確無語的保證所寫動態SQL沒有問題。
總結
本節我們闡述了動態SQL以及要注意的地方,同時為了便於調試保證所生成的SQL語句不會出現問題給出的建議。下節我們開始講死鎖問題,歡迎大家持續關注博客和公眾號,對於.NET Core也會持續更新。See u!