本文出處: http://www.cnblogs.com/wy123/p/5958047.html php
最近發現還有很多作開發的小夥伴,在寫存儲過程的時候,在參考已有的不一樣的寫法時,每每很迷茫,
不知道各類寫法孰優孰劣,該選用那種寫法,以及各類寫法優缺點,本文以一個簡單的查詢存儲過程爲例,簡單說一下各類寫法的區別,以及該用那種寫法
專業DBA以及熟悉數據庫的同窗請無視。html
廢話很少,上代碼說明,先造一個測試表待用,簡單說明一下這個表的狀況sql
相似訂單表,訂單表有訂單ID,客戶ID,訂單建立時間等,查詢條件是經常使用的訂單ID,客戶ID,以及訂單建立時間數據庫
create table SaleOrder ( id int identity(1,1), OrderNumber int , CustomerId varchar(20) , OrderDate datetime , Remark varchar(200) )GOdeclare @i int=0 while @i<100000 begin insert into SaleOrder values (@i,CONCAT('C',cast(RAND()*1000 as int)),GETDATE()-RAND()*100,NEWID()) set @i=@i+1 end create index idx_OrderNumber on SaleOrder(OrderNumber) create index idx_CustomerId on SaleOrder(CustomerId) create index idx_OrderDate on SaleOrder(OrderDate)
生成的測試數據大概就是這個樣子的安全
下面演示說明幾種常見的寫法以及每種寫法潛在的問題服務器
第一種常見的寫法:拼湊字符串,用EXEC的方式執行這個拼湊出來的字符串,不推薦ide
create proc pr_getOrederInfo_1 ( @p_OrderNumber int , @p_CustomerId varchar(20) , @p_OrderDateBegin datetime , @p_OrderDateEnd datetime ) as begin set nocount on; declare @strSql nvarchar(max); set @strSql= 'SELECT [id] ,[OrderNumber] ,[CustomerId] ,[OrderDate] ,[Remark] FROM [dbo].[SaleOrder] where 1=1 '; /* 這種寫法的特色在於將查詢SQL拼湊成一個字符串,最後以EXEC的方式執行這個SQL字符串 */ if(@p_OrderNumber is not null) set @strSql = @strSql + ' and OrderNumber = ' + @p_OrderNumber if(@p_CustomerId is not null) set @strSql = @strSql + ' and CustomerId = '+ ''''+ @p_CustomerId + '''' if(@p_OrderDateBegin is not null) set @strSql = @strSql + ' and OrderDate >= ' + '''' + cast(@p_OrderDateBegin as varchar(10)) + '''' if(@p_OrderDateEnd is not null) set @strSql = @strSql + ' and OrderDate <= ' + '''' + cast(@p_OrderDateEnd as varchar(10)) + '''' print @strSql exec(@strSql); end
假如咱們查詢CustomerId爲88,在2016-10-1至2016-10-3這段時間內的訂單信息,以下,帶入參數執行測試
exec pr_getOrederInfo_1 @p_OrderNumber = null , @p_CustomerId = 'C88' , @p_OrderDateBegin = '2016-10-1' , @p_OrderDateEnd = '2016-10-3'
首先說明,這種方式執行查詢是徹底沒有問題的以下截圖,結果也查出來了(固然結果也是沒問題的)優化
咱們把執行的SQL打印出來,執行的SQL語句自己就是就是存儲過程當中拼湊出來的字符串,這麼一個查詢SQL字符串spa
SELECT [id] ,[OrderNumber] ,[CustomerId] ,[OrderDate] ,[Remark] FROM [dbo].[SaleOrder] where 1=1 and CustomerId = 'C88' and OrderDate >= '2016-10-1' and OrderDate <= '2016-10-3'
那麼這種存儲過程的有什麼問題,或者直接一點說,這種方式有什麼很差的地方
其一,繞不過轉移符(以及注入問題)
在拼湊字符串時,把全部的參數都當成字符串處理,當查詢條件自己包含特殊字符的時候,好比 ' 符號,
或者其餘須要轉義的字符時,你拼湊的SQL就被打斷了
舉個不恰當的例子,好比字符串中 @p_CustomerId中包含 ' 符號,直接就把你拼SQL的節湊給打亂了
拼湊的SQL就變成了這個樣子了,語法就不經過,更別提執行
SELECT [id] ,[OrderNumber] ,[CustomerId] ,[OrderDate] ,[Remark] FROM [dbo].[SaleOrder] where 1=1 and CustomerId = 'C'88'
一方面須要處理轉移符,另外一方面須要要防止SQL注入
其二,參數不一樣就必須從新編譯
這種拼湊SQL的方式,若是每次查詢的參數不一樣,拼湊出來的SQL字符串也不同,
若是熟悉SQL Server的同窗必定知道,只要你執行的SQL文本不同,
好比
第一次是執行查詢 *** where CustomerId = 'C88' ,
第二次是執行查詢 *** where CustomerId = 'C99' ,由於兩次執行的SQL文本不一樣
每次執行以前必然須要對其進行編譯,編譯的話就須要CPU,內存資源
若是存在大批量的SQL編譯,無疑要消耗更多的CPU資源(固然也須要一些內存資源)
第二種常見的寫法:對全部查詢條件用OR的方式加在where條件中,很是不推薦
create proc pr_getOrederInfo_2 ( @p_OrderNumber int , @p_CustomerId varchar(20) , @p_OrderDateBegin datetime , @p_OrderDateEnd datetime ) as begin set nocount on; declare @strSql nvarchar(max); SELECT [id] ,[OrderNumber] ,[CustomerId] ,[OrderDate] ,[Remark] FROM [dbo].[SaleOrder] where 1=1 and (@p_OrderNumber is null or OrderNumber = @p_OrderNumber) and (@p_CustomerId is null or CustomerId = @p_CustomerId) /* 這是另一種相似的奇葩的寫法,下面會重點關注 and OrderNumber = ISNULL( @p_OrderNumber,OrderNumber) and CustomerId = ISNULL( @p_CustomerId,CustomerId) */ and (@p_OrderDateBegin is null or OrderDate >= @p_OrderDateBegin) and (@p_OrderDateEnd is null or OrderDate <= @p_OrderDateEnd) end
首先看這種方式的執行結果,帶入一樣的參數,跟上面的結果同樣,查詢(結果)自己是沒有任何問題的
這種寫法寫起來避免了拼湊字符串的處理,看起來很簡潔,寫起來也很快,稀里嘩啦一個存儲過程就寫好了,
發佈到生產環境以後就至關於埋了一顆雷,隨時引爆。
由於一條低效而又頻繁執行的SQL,拖垮一臺服務器也是司空見慣
可是呢,問題很是多,也很是很是不推薦,甚至比第一種方式更糟糕。
分析一下這種處理方式的邏輯:
這種處理方式,由於不肯定查詢的時候到底有沒有傳入參數,也就數說不能肯定某一個查詢條件是否生效,
因而就採用相似 and (@p_OrderNumber is null or OrderNumber = @p_OrderNumber)這種方式,來處理參數,
這樣的話
若是@p_OrderNumber爲null,or的前者(@p_OrderNumber is null)成立,後者不成立,查詢條件不生效
若是@p_OrderNumber爲非null,or的後者(OrderNumber = @p_OrderNumber)成立而前者不成立,查詢條件生效
總之來講,無論參數是否爲空,均可以有效地拼湊到查詢條件中去。
避免了拼SQL字符串,既作到讓參數非空的時候生效,有作到參數爲空的時候不生效,看起來不錯,是真的嗎?
那麼這種存儲過程的有什麼問題?
1,會抑制索引的狀況
如圖,帶入參數值執行存儲過程,先忽略另外三個查詢字段,只傳入@p_CustomerId參數,
相關查詢列上(CustomerId)有索引,可是這裏走的是CustomerId列上的Index Scan而非預期的Index Seek
爲何說可能會抑制到索引的時候?上面提到過,SQL在執行以前是須要編譯的,
由於在編譯的時候並不知道查詢條件是否傳入了值,有可能爲null,有多是一個具體的值
糾錯:上面的一句話,使用參數作編譯的時候,是知道參數的值的(只有使用本地變量的時候纔不知道具體的參數值,直接使用參數確實是知道的),
編譯也是根據具體的參數值來生成執行計劃的,可是爲何即便知道具體的參數值的狀況下,依然生成一個Index Scan的方式,而不是指望的Index Seek?
即使是存儲過程在編譯的時候知道了參數的值,爲何仍舊用不到索引?
還要從and (@p_CustomerId is null or CustomerId = @p_CustomerId)這種寫法入手分析。
即使是CustomerId列上有索引,
若是@p_CustomerId 參數非空,走索引Seek徹底沒有問題。
若是@p_CustomerId 爲null,此時and (@p_CustomerId is null or CustomerId = @p_CustomerId)這個條件恆成立,若是再走索引Seek會出現什麼結果?
語義上變成了是查找CustomerId 爲null的值,若是採用Index Seek的方式執行,這樣的話邏輯上已經錯誤了。
所以出現這種寫法,爲了安全起見,優化器只能選擇一個索引的掃描(即使是字段上有索引的狀況下)
能夠認爲是這種寫法在語義支持不了相關索引的Seek,而索引的Scan是處理這種寫法的一種安全的方式
The optimiser can tell that and it plays safe. It creates plans that will always work.
That’s (one of the reasons) why in the first example it was an index scan, not an index seek.
參考這裏,能夠簡單地理解成這種寫法,語義上支持不了索引的Seek,最多支持到index scan
至於(@p_CustomerId is null or CustomerId = @p_CustomerId )這種寫法遇到本地變量的時候,
爲何抑制到到索引的使用,我以前也是沒有弄清楚的,評論中10樓Uest 給出瞭解釋,這裏很是感謝Uest
以下
若是我直接帶入CustomerId=‘C88’,再來看執行計劃,結果跟上面同樣,可是執行計劃是徹底不同的,這就是所謂的抑制到索引的使用。
2,很是很是致命的邏輯錯誤
/* 這是另一種相似的奇葩的寫法,須要重點關注,真的就能知足「無論參數是否爲空都知足」 and OrderNumber = ISNULL( @p_OrderNumber,OrderNumber) and CustomerId = ISNULL( @p_CustomerId,CustomerId) */
對於以下這種寫法:OrderNumber = ISNULL( @p_OrderNumber,OrderNumber),
一部分人很是推崇,認爲這種方式簡單、清晰,我也是醉了,有可能產生很是嚴重的邏輯錯誤
若是參數爲null,就轉換成這種語義 where 1=1 and OrderNumber = OrderNumber
目的是查詢參數爲null,查詢條件不生效,讓這個查詢條件恆成立,恆成立嗎,不必定,某些狀況下就會有嚴重的語義錯誤
博主發現這個問題也是由於某些實際系統中的bug,折騰了很久才發現這個嚴重的邏輯錯誤 http://www.cnblogs.com/wy123/p/5580821.html
對於這種寫法,
不論是第一點說的抑制索引的問題,數據量大的時候是很是嚴重的,上述寫法會形成全表(索引)掃描,有索引也用不上,至於全表(索引)掃描的壞處就不說了
仍是第二點說的形成的邏輯錯誤,都是很是致命的
因此這種方式是最最不推薦的。
第三種常見的寫法:參數化SQL,推薦
create proc pr_getOrederInfo_3 ( @p_OrderNumber int , @p_CustomerId varchar(20) , @p_OrderDateBegin datetime , @p_OrderDateEnd datetime ) as begin set nocount on; DECLARE @Parm NVARCHAR(MAX) = N'', @sqlcommand NVARCHAR(MAX) = N'' SET @sqlcommand = 'SELECT [id] ,[OrderNumber] ,[CustomerId] ,[OrderDate] ,[Remark] FROM [dbo].[SaleOrder] where 1=1 ' IF(@p_OrderNumber IS NOT NULL) SET @sqlcommand = CONCAT(@sqlcommand,' AND OrderNumber= @p_OrderNumber') IF(@p_CustomerId IS NOT NULL) SET @sqlcommand = CONCAT(@sqlcommand,' AND CustomerId= @p_CustomerId') IF(@p_OrderDateBegin IS NOT NULL) SET @sqlcommand = CONCAT(@sqlcommand,' AND OrderDate>=@p_OrderDateBegin ') IF(@p_OrderDateEnd IS NOT NULL) SET @sqlcommand = CONCAT(@sqlcommand,' AND OrderDate<=@p_OrderDateEnd ') SET @Parm= '@p_OrderNumber int, @p_CustomerId varchar(20), @p_OrderDateBegin datetime, @p_OrderDateEnd datetime ' PRINT @sqlcommand EXEC sp_executesql @sqlcommand,@Parm, @p_OrderNumber = @p_OrderNumber, @p_CustomerId = @p_CustomerId, @p_OrderDateBegin = @p_OrderDateBegin, @p_OrderDateEnd = @p_OrderDateEnd end
首先咱們用一樣的參數來執行一下查詢,固然沒問題,結果跟上面是同樣的。
所謂的參數化SQL,就是用變量當作佔位符,經過 EXEC sp_executesql執行的時候將參數傳遞進去SQL中,在須要填入數值或數據的地方,使用參數 (Parameter) 來給值,
這樣的話,
第一,既能避免第一種寫法中的SQL注入問題(包括轉移符的處理),
由於參數是運行時傳遞進去SQL的,而不是編譯時傳遞進去的,傳遞的參數是什麼就按照什麼執行,參數自己不參與編譯
第二,保證執行計劃的重用,由於使用佔位符來拼湊SQL的,SQL參數的值不一樣並致使最終執行的SQL文本不一樣
同上面,參數自己不參與編譯,若是查詢條件同樣(SQL語句就同樣),而參數不同,並不會影響要編譯的SQL文本信息
第三,還有就是避免了第二種狀況(and (@p_CustomerId is null or CustomerId = @p_CustomerId)
或者 and OrderNumber = ISNULL( @p_OrderNumber,OrderNumber))
這種寫法,查詢條件有就是有,沒有就是沒有,不會丟給SQL查詢引擎一個模棱兩個的結果,
避免了對索引的抑制行爲,是一種比較好的處理查詢條件的方式。
缺點,1,對於這種方式,也有一點很差的地方,就是拼湊的字符串處理過程當中,
調試具體的SQL語句的時候,參數是直接拼湊在SQL文本中的,不能直接執行,要手動將佔位參數替換成具體的參數值
2,可能存在parameter sniff問題,可是對於parameter sniff問題,不是否認參數化SQL的重點,固然解決parameter sniff問題的辦法仍是有的,
參考:http://www.cnblogs.com/wy123/p/5645485.html
總結:
以上總結了三種在開發中比較常見的存儲過程的寫法,每種存儲過程的寫法可能在不一樣的公司都用應用, 是否是有人挑個最簡單最快捷(第二種)寫法,寫完不是完事了,而是埋雷了。 不是太熟悉SQL Server的同窗可能會有點迷茫,有不少種寫法,究竟要用哪一種寫法這些寫法之間有什麼區別。 本文經過一個簡單的示例,說了常見的幾種寫法之間的區別,每種方式存在的問題,以及孰優孰劣,請小夥伴們明辨。 數據庫大神請無視,謝謝。