接上文:T-SQL動態查詢(3)——靜態SQL前端
前面說了很是多關於動態查詢的內容。本文將介紹使用動態SQL解決動態查詢的一些方法。sql
在很是多項目中,動態SQL被普遍使用甚至濫用。很是多時候,動態SQL又確實是解決很是多需求的首選方法。但是假設不合理地使用,會致使性能問題及沒法維護。動態SQL尤爲本身的優缺點。是否使用需要進行評估分析:數據庫
本文出處:http://blog.csdn.net/dba_huangzj/article/details/50202371編程
靜態SQL事實上可以應對大部分的平常需求。但是隨着需求的添加,靜態SQL會變得愈來愈複雜,同一時候可能帶來過多的重編譯,此時應該考慮動態SQL。緩存
在SQL Server中,動態SQL可以由三種方式實現:架構
本文着重介紹T-SQL中的存儲過程。針對用戶的輸入,有兩種方式進行處理:編程語言
基於很是多理由,在平常使用中。推薦使用第二種方法也就是sp_executesql。函數
但是需要提醒的是上面提到的三種實現動態SQL的方式沒有本質上的好和壞。僅僅有依據實際狀況而定纔是最有效的。工具
本文將使用靜態SQL篇中的需求做爲演示,即針對不一樣的查詢條件、不一樣的排序甚至不一樣的彙總需求演示。post
本文出處:http://blog.csdn.net/dba_huangzj/article/details/50202371
對於存儲過程當中使用靜態SQL,權限問題並沒有大礙。僅僅要存儲過程的調用者和表的擁有者是一樣的。因爲所有權鏈(ownership chaining,https://msdn.microsoft.com/zh-cn/library/ms188676.aspx),可以無障礙地運行存儲過程。
但是動態SQL中不存在所有權鏈,即便把它們放在存儲過程當中也同樣,因爲動態SQL有本身的權限範圍。
假設在client程序或CLR存儲過程當中建立動態SQL,還需要額外授予用戶具備查詢中涉及到的表、視圖、本身定義函數上的SELECT權限。
依據client程序和CLR存儲過程的不一樣,權限鏈可能會很是混亂和失控。但是可以使用如下兩種方式來應付:
本部分使用第一篇中提到的模版進行改造演示。爲了能清晰地描寫敘述。使用博客自帶的行號來標號:
USE [AdventureWorks2008R2] GO CREATE PROCEDURE [dbo].[sp_Get_orders] @salesorderid int = NULL, @fromdate datetime = NULL, @todate datetime = NULL, @minprice money = NULL, @maxprice money = NULL, @custid int = NULL, @custname nvarchar(40) = NULL, @prodid int = NULL, @prodname nvarchar(40) = NULL, @employeestr varchar(MAX) = NULL, @employeetbl intlist_tbltypeREADONLY, @debug bit =0 AS DECLARE @sql nvarchar(MAX), @paramlist nvarchar(4000), @nl char(2) = char(13) + char(10) SELECT @sql=' SELECT o.SalesOrderID, o.OrderDate, od.UnitPrice,od.OrderQty, c.CustomerID, per.FirstName as CustomerName,p.ProductID, p.Name as ProductName, per.BusinessEntityID as EmpolyeeID FROM Sales.SalesOrderHeader o INNER JOIN Sales.SalesOrderDetail od ON o.SalesOrderID= od.SalesOrderID INNER JOIN Sales.Customer c ON o.CustomerID =c.CustomerID INNER JOIN Person.Person per onc.PersonID=per.BusinessEntityID INNER JOIN Production.Product p ON p.ProductID =od.ProductID WHERE 1=1'+@nl IF @salesorderidIS NOT NULL SELECT @sql+= ' AND o.SalesOrderID=@SalesOrderID'+ ' ANDod.SalesOrderID=@SalesOrderID'+@nl IF @fromdateIS NOT NULL SELECT @sql+= ' AND o.OrderDate >= @fromdate'+@nl IF @todateIS NOT NULL SELECT @sql+= ' AND o.OrderDate <= @todate'+@nl IF @minpriceIS NOT NULL SELECT @sql += 'AND od.UnitPrice >= @minprice' + @nl IF @maxpriceIS NOT NULL SELECT @sql += 'AND od.UnitPrice <= @maxprice' + @nl IF @custidIS NOT NULL SELECT @sql += 'AND o.CustomerID = @custid' + ' AND c.CustomerID = @custid' +@nl IF @custnameIS NOT NULL SELECT@sql += ' AND per.FirstName LIKE @custname + ''%''' + @nl IF @prodidIS NOT NULL SELECT@sql += ' AND od.ProductID = @prodid' + ' AND p.ProductID = @prodid' +@nl IF @prodnameIS NOT NULL SELECT@sql += ' AND p.Name LIKE @prodname + ''%''' + @nl IF @employeestrIS NOT NULL SELECT@sql += ' AND per.BusinessEntityID IN' + ' (SELECT number FROM dbo.intlist_to_tbl(@employeestr))'+ @nl IF EXISTS(SELECT * FROM @employeetbl) SELECT@sql += ' AND per.BusinessEntityID IN (SELECT val FROM @employeetbl)'+ @nl SELECT @sql+= ' ORDER BYo.SalesOrderID' + @nl IF @debug= 1 PRINT @sql SELECT @paramlist= '@salesorderid int, @fromdate datetime, @todate datetime, @minprice money, @maxprice money, @custid nchar(5), @custname nvarchar(40), @prodid int, @prodname nvarchar(40), @employeestr varchar(MAX), @employeetbl intlist_tbltype READONLY' EXEC sp_executesql@sql, @paramlist,@salesorderid, @fromdate, @todate, @minprice, @maxprice, @custid, @custname,@prodid, @prodname, @employeestr, @employeetbl
在上面代碼中的第18行。定義了一個變量@sql。用於存儲查詢字符串。
因爲sp_executesql要求參數必須爲NVARCHAR,因此這裏使用NVARCHAR(MAX),以便足夠存放所有終於字符串。
在第20行,使用了一個變量@nl,經過賦值char(13)+char(10)實現Windows上的換行符功能。儘管它是變量,但是在存儲過程當中其實是一個常量。
在第22 到31行。包括了動態SQL的核心部分,並存放在@sql變量中。經過興許的參數拼接實現整個動態SQL查詢。
注意代碼中均使用了兩部命名(即架構名.表名),因爲因爲性能緣由。SQL Server在編譯和優化時需要精肯定位對象,假設表A存在dbo.A和Sales.A這兩個架構名,那麼SQL Server需要花時間去推斷到底使用的是哪一個表,這會帶來不小的開銷,注意。即便僅僅有幾十毫秒,但是對於一個頻繁被運行的存儲過程或語句,整體性能會被明顯拉低,因此不管基於性能仍是編程規範的考慮,都應該帶上架構名。固然假設你的系統僅僅有dbo這個默認架構,不帶也行,但是建議仍是要規範化編程提升可讀性和可維護性。
這裏再插一句,在本人優化的代碼中。經常看到很是多語句中。表名使用了別名,但是在ON、WHERE中又沒有帶上別名前綴,咋一看上去很是難知道字段來自於哪一個表,要一個一個相關表去檢查。花了不應花的時間,爲了維護代碼的人,大家便可行好吧。
在第31行是一句「WHERE 1=1」,類似編程語言中的佔位符。使WHERE語句即便單獨存在也不會報錯。如下會介紹爲何也要加上@nl。
在第33行開始。針對所有單值查詢參數進行檢查,假設參數不爲NULL,則加入到終於的SQL字符串的相應列中。從這裏開始就要注意對單引號、雙引號的使用,同一時候留意在每次拼接後面都加上了@nl。
在第67 行,對@employeestr參數進行處理,處理方式和上一篇靜態SQL同樣。其它剩餘部分相對簡單,不作過多解釋。
在第72行。加入了一個參數@debug。默以爲0,當用戶調用傳入1時,輸出SQL字符串,這在調試和檢查錯誤時很是實用,因爲動態SQL每每很是難直接從代碼中看出終於語句,假設在開發過程沒有注意引號、空格、類型轉換等問題時,都會在興許調用過程當中報錯。經過@debug參數,可以在未運行語句(即還不至於報錯中止以前)就把需要運行的語句打印出來,注意順序很是重要,假設在運行報錯後你再想打印就不必定能打印出來了。
對於差點兒每行後面都加入的@nl。固然是有意圖的。假設不加換行符,代碼可能會變成單行很是長的字符串,print出來不直觀。
甚至看起來很是痛苦,儘管現在有格式化工具,但是不是每次都破解成功,對單串字符串的美化仍是比較浪費時間的。
最後,經過sp_executesql運行SQL字符串,這是一個系統存儲過程。需要提供兩個固定參數,第一個是SQL字符串,第二個是參數列。這些參數必須是nvarchar類型。
在這個樣例中。調用語句在存儲過程內部。你也可以在外部調用存儲過程。但是需要記住的是動態SQL不能得知不論什麼調用參數。
注意存儲過程最後的參數列@paramlist,是靜態的,也就是參數集是固定的,即便有些參數並不是每次都會使用到。
可以使用如下語句對存儲過程進行測試:
EXEC [sp_Get_orders]@salesorderid = 70467 EXEC [sp_Get_orders]@custid = 30097 EXEC [sp_Get_orders]@prodid = 936 EXEC [sp_Get_orders]@prodid = 936, @custname = 'Carol' EXEC [sp_Get_orders]@fromdate = '2007-11-01 00:00:00.000', @todate = '2008-04-18 00:00:00.000' EXEC [sp_Get_orders]@employeestr = '20124,759,1865', @custid = 29688 DECLARE @tbl intlist_tbltype INSERT @tbl(val) VALUES(20124),(759),(1865) EXEC [sp_Get_orders]@employeetbl = @tbl, @custid = 29688對於這類狀況,需要對所有參數進行測試。最好是能知道實際使用中哪些參數的使用頻率最高。
每當用戶以一樣查詢參數集進行調用這個存儲過程時,運行計劃會被重用。假設調用上一章的存儲過程sp_get_orders_1時,如:
EXEC sp_get_orders_1@salesorderid = 70467 EXEC sp_get_orders_1@salesorderid = 70468 EXEC sp_get_orders_1@salesorderid = 70469
因爲OPTION(RECOMPILE),因此不緩存不論什麼運行計劃並且每次都重編譯。但是對於本文中的存儲過程:
EXEC [sp_Get_orders]@salesorderid = 70467 EXEC [sp_Get_orders]@salesorderid = 70468 EXEC [sp_Get_orders]@salesorderid = 70469
僅僅會針對第一次調用進行編譯並緩存運行計劃,興許兩次調用將使用第一的運行計劃進行直接運行。但是當調用的參數變化時,如:
EXEC [sp_Get_orders]@salesorderid = 70467,@prodid = 870
會發生新的編譯併產生新的緩存條目,但原有的用於查詢SalesOrderID的運行計劃不受影響。
在上一篇靜態SQL中,已經展現了怎樣用靜態SQL實現某些特殊的查詢條件,本部分將演示用動態SQL來完畢這些工做。前面提到過,靜態SQL針對簡單的查詢條件,足以應付自如,但是當需求數量和複雜度逐步添加時。靜態SQL將變得不可控。此時就需要考慮動態SQL。
在很是多系統中,常見的一類狀況是,訂單表上有一個狀態列Status。裏面有4個值:N(新訂單)、P(處理中)、E(異常訂單)、C(已處理訂單)。同一時候差點兒99%的數據都是爲C。
這樣的狀況下可以使用對該列中C值的過濾索引/篩選索引(filterindex)來過濾沒必要要的數據或需要經常查詢的數據。
但是假設在動態SQL中這樣寫:
IF @status IS NOT NULL SELECT @sql += ' AND o.Status = @status'
因爲動態SQL的運行計劃是針對所有狀況進行優化的,因此這樣的寫法是不會專門針對過濾索引發效,需要額外製定一些操做邏輯來「指示」優化器使用這個過濾索引,如:
IF @status IS NOT NULL SELECT @sql += ' AND o.Status = @status' + CASE WHEN @status <> 'C' THEN ' AND o.Status <> ''C''' ELSE '' END這樣的狀況是針對單值參數,假設@status爲多值。即用戶需要篩選某些類型的數據,則需要按這樣的方式加入不少其它的處理邏輯。
在動態SQL中,很是常見的應用常見是使用本身定義的排序規則。經過用戶前端輸入的排序條件進行結果集排序。比方:
@sql += ' ORDER BY ' + @sortcol
這樣的寫法可以知足多列排序。比方’SalesOrderID, OrderTime Desc’。儘管對於知足功能來講。已經足夠了,但是因爲client不知道查詢自己。可能致使傳入的參數不屬於相關的表或其它因素致使報錯,特別是ORDER BY在T-SQL的邏輯處理中屬於接近最後部分。SELECT語句可能把原始列進行重命名、運算等,致使前端沒法得知SELECT的終於列名。另外即便是使用了正確的名字,但是在興許可能因爲表結構的變動、列名變動等因素又帶來報錯。
這樣的狀況事實上很是難避免。只是多考慮一下問題可能就沒有那麼嚴重,比方可以用如下的方式來預處理:
SELECT @sql += ' ORDER BY ' + CASE @sortcol WHEN 'OrderID' THEN 'o.OrderID' WHEN 'EmplyoeeID' THEN 'o.EmployeeID' WHEN 'ProductID' THEN 'od.ProductID' WHEN 'CustomerName' THEN 'c.CompanyName' WHEN 'ProductName' THEN 'p.ProductName' ELSE 'o.OrderID' END + CASE @isdesc WHEN 0 THEN ' ASC' ELSE ' DESC' END
在上一章備用表中。提到了關於不一樣參數訪問不一樣表的狀況。這樣的狀況在動態SQL中實現也不難,可以把FROM部分改寫成:
ROM dbo.' + CASE @ishistoric WHEN 0 THEN 'Orders' WHEN 1 THEN 'HistoricOrders' END + ' o JOIN dbo.' + CASE @ishistoric WHEN 0 THEN '[Order Details]' WHEN 1 THEN 'HistoricOrderDetails' END + ' od
本文出處:http://blog.csdn.net/dba_huangzj/article/details/50202371
參數化動態SQL的當中一個優點是可以經過計劃重用而下降編譯次數。但是緩存並不老是好的。比方在上一章基礎技能部分提到的:
1. exec sp_Get_orders_1@fromdate='20050701',@todate ='20050701' 2. exec sp_Get_orders_1@fromdate='20050101',@todate ='20051231'
儘管參數集一樣,但是當值不一樣的時候,假設這些不一樣的值的數據分佈嚴重不均勻,會致使運行計劃沒法高效支持所有查詢。
這樣的狀況在動態SQL和靜態SQL中都比較常見,如下來介紹一下處理方法:
對,你又見到它了。在上面提到的特定狀況下,假設查詢條件是@fromdate和@todate,加入OPTION(RECOMPILE):
IF (@fromdate IS NOT NULL OR @todate IS NOT NULL) SELECT @sql += ' OPTION(RECOMPILE)' + @nl
有時候可以嘗試使用「提示,hints」。可以經過CASE WHEN 推斷需要傳入什麼參數。並且對這些參數額外指定需要走的索引。
但是正如前面提到過的。提示要慎用。特別是索引提示,除非你確保索引名永不變動:
FROM dbo.Orders o ' + CASE WHEN @custid IS NOT NULL AND (@fromdate IS NOT NULL OR @todate IS NOT NULL) THEN 'WITH (INDEX = CustomerID) ' ELSE '' END
第二種提示是使用OPTIMIZE FOR。假設你但願運行計劃老是使用佔用最多的狀況來編譯,比方前面提到的status類型中的C,那麼可以加入:
IF @status IS NOT NULL @sql += ' OPTION (OPTIMIZE FOR (@status = ''C''))'
IF @fromdate IS NOT NULL AND @todate IS NOT NULL @sql += ' OPTION (OPTIMIZE FOR (@fromdate UNKNOWN, @todate UNKNOWN))'
這樣優化器就不會使用標準假設,即10%左右來編譯查詢。
本文出處:http://blog.csdn.net/dba_huangzj/article/details/50202371
動態SQL很是強大。但是假設讀者概括能力比較強的話,可以看到,動態SQL的問題主要是在不能很是好地利用計劃緩存或使用的是不合適的運行計劃,致使性能問題。
對於這類狀況,有很是多方法可以使用。並且假設可以,最好仍是考慮非數據庫層面的其它技術。
但是咱們的目的仍是一個:保證運行計劃針對不論什麼參數,最起碼絕大部分參數都是最佳的。並且可以儘量重用。
最後。需要提醒的是。不論什麼技術、技巧。都應該在儘量貼近實際環境的測試環境中作充分的測試,以便獲得你但願的結果。
本文出處:http://blog.csdn.net/dba_huangzj/article/details/50202371