最近發現一個分頁查詢存儲過程當中的的一個SQL語句,當彙集索引列的排序方式不一樣的時候,效率差異達到數十倍,讓我感到很是吃驚
由此引起出來分頁查詢的狀況下對大表作Clustered Scan的時候,
不一樣狀況下會選擇FORWARD 或者 BACKWARD差異,以及創建彙集索引時,選擇索引列的排序方式的一些思考
廢話很少,上代碼
先創建一張測試表,在Col1上創建彙集索引,寫入100W條數據sql
create table ClusteredIndexScanDirection ( Col1 int identity(1,1), Col2 varchar(50), Col3 varchar(50), Col4 Datetime ) create unique clustered index idx_Col1 on ClusteredIndexScanDirection(Col1 ASC) DECLARE @date datetime,@i int=0 set @date=GETDATE() while @i<1000000 begin insert into ClusteredIndexScanDirection values (NEWID(),NEWID(),DATEADD(MI,@i,GETDATE()-200)) set @i=@i+1 end
先直觀地看一下彙集索引掃描時候的FORWARD 和 BACKWARDide
BACKWARD性能
執行以下分頁查詢,當按照Col4符合2017-7-18和2017-7-23,而且Col1 倒序排序的時候
從執行計劃看,Clustered Index Scan的Scan Direction的方式是BACKWARD測試
FORWARD優化
執行以下分頁查詢,當按照Col4符合2017-7-18和2017-7-23,而且Col1 正序排序的時候
從執行計劃看,Clustered Index Scan的Scan Direction的方式是FORWARDspa
查詢條件同樣,分頁狀況下,排序方式不同,性能上有麼有差異?確定有,太明顯了,若是沒有,本文也就沒有什麼意義了
如圖是上述兩種查詢方式在我本機的測試結果,一樣是前100條數據,由於排序方式不一樣,其代價也是不一樣的
邏輯讀,一個是2327,一個是9978次,差異不小吧,在實際場景中,這個差異是很是很是大的,大到足以超乎你想一想code
對FORWARD和BACKWARD有一個直觀的感覺以後,來講說這二者的區別blog
若是瞭解B樹索引結構的話,應該知道彙集索引是以相似於B樹結構的方式來組織的,既然是B樹結構,
那麼下面這個圖就不難理解了,
在索引列按照某事方式排序的狀況下,好比排序
create unique clustered index idx_Col1 on ClusteredIndexScanDirection(Col1 ASC)
或者是
create unique clustered index idx_Col1 on ClusteredIndexScanDirection(Col1 DESC)
下面這張圖分別是FORWARD和BACKWARD兩種Scan direction的實現方式索引
FORWARD
BACKWARD
Sql Server究竟選中哪一種方式,是FORWARD仍是BACKWARD,是依賴於你的索引狀況和查詢結果集排序狀況的
以我上面的查詢爲例
若是是按照查詢結果正序排序的方式查詢
SELECT * FROM ClusteredIndexScanDirection WITH (NOLOCK) WHERE Col4 >= '2017-7-18' AND Col4 <= '2017-7-23' ORDER BY 1 ASC OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY
也就是要求查詢結果的排序方式與彙集索引的排序方式一致,彙集索引是ASC的,Sql Server就會採用FORWARD的方式,
也便是從左到右的Scan方式,找到知足1000條的數據後返回,查詢終止
若是是按照查詢結果的倒序排序的方式查詢
SELECT * FROM ClusteredIndexScanDirection WITH (NOLOCK) WHERE Col4 >= '2017-7-18' AND Col4 <= '2017-7-23' ORDER BY 1 DESC OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY
也就是要求查詢結果的排序方式與彙集索引的排序方式不一致,彙集索引是ASC的,Sql Server就會採用BACKWARD的方式,
也便是從右到左的Scan方式,找到知足100條的數據後返回,查詢終止
如今就存在一個問題,若是彙集索引是按照ASC正序排列的,也就是說在彙集索引排序必定的狀況下,
彙集索引列和查詢條件(CreateDate)上的時候都是遞增的,也就是說,查詢目標數據分佈在B樹的右邊,
(固然這麼說不嚴謹,物理存儲中並無左右的概念,這些都是邏輯上的,並非徹底物理上的概念),
實際業務中,差很少的意思就是查詢最近N天的數據
若是查詢結果是按照彙集索引正序排序,
Sql Server 採用FORWARD的方式,也即從左至右,那麼這個查詢就要經歷B樹種從左到右很大一部分數據掃描以後,才能找到所須要的數據
若是查詢結果是按照彙集索引倒敘排序,
Sql Server 採用BACKWARD的方式,也即從右至左,那麼這個查詢直接從最右邊開始Scan,很快就能找到符合條件的100條數據。
彙集索引是ASC或者DESC的方式,也會影響到這個查詢,這些概念都是相對的,固然實際場景中,索引狀況和查詢條件可能更復雜,
可見,一個查詢的實現,是經過FORWARD仍是BACKWARD,跟彙集索引的排序方式和查詢結果的排序方式,以及查詢條件都有關。
Sql Server 選擇FORWARD或者BACKWARD,自己都沒有錯,若是出現不一樣排序方式下性能差異很是大的時候,
就要注意到是否是,彙集索引的方式與查詢排序方式之間存在相似上述的問題。
不論是FORWARD或者BACKWARD,避免讓Scan整個表的大部分數據才找到符合條件的數據
固然實際狀況也比例子中複雜不少,仍是那句話,具體狀況具體分析。
好比業務系統查詢數據時,排序方式是固定的(好比你網購的訂單信息,老是按照時間倒敘排列的),固然也不排除其餘狀況
這就要求咱們在建立彙集索引的時候,要考慮到查詢的方式以及排序的方式,慎重地做出選擇。
總結:
SQLServer在對查詢結果排序的查詢中,若是掃描的方向與查詢結果不一致,須要再次在內存中排序,
所以,大多數狀況下,會根據查詢結果的排序來執行FORWARD或者BACKWARD操做(固然也不必定百分百)。
本文經過彙集索引Scan的兩種方式,FORWARD和BACKWARD,粗淺第分析了表上的彙集索引的排序對查詢時的影響,
固然非彙集索引上也會出現FORWARD和BACKWARD掃描的請,
咱們在選擇彙集索引排序方式的時候,能夠考慮到是否是由於FORWARD和BACKWARD的因素,以便進一步的排查確認。
補充:
好吧,算我沒說清楚,這裏是按照彙集索引排序,按照非索引字段查詢,而不是直接按照彙集索引字段查詢!!!
個人例子已經寫的很清楚了
若是彙集索引創建在一個字段上,也即單字段做爲彙集索引,在非彙集索引字段上查詢,暫不論這個字段上有沒有索引
若是查詢結果的跟彙集索引的排序方式是相同的,那麼就是FORWARD
若是查詢結果的跟彙集索引的排序方式是相反的,那麼就是BACKWARD
不論是FORWARD仍是BACKWARD,究竟要掃描多大範圍才能找到符合條件的數據,
取決於上面說的非彙集索引字段列的數據分佈,豈能說「 正序和倒序無差異」?
其實我更想表達的是,由於結果集的排序,會致使在作彙集索引Scan的時候選擇FORWARD或者BACKWARD
FORWARD仍是BACKWARD會對查詢的效率有較大的影響,
實際應用中太複雜了,固然修改彙集索引的排序方式能夠從必定程度上緩解這種問題,我固然測試過,否則也不會亂說
也有其餘方法也能夠實現,好比暴力地去修改彙集索引列,或者創建複合彙集索引,辦法也不只限於此
若是還有不明白的,能夠試試下面這個腳本,能夠直接在你機器上執行,看看最後兩個查詢的IO代價
固然這個例子也比較極端
create table ClusteredIndexScanDirection ( Col1 int identity(1,1), Col2 varchar(50), Col3 varchar(50), Col4 Datetime ) create unique clustered index idx_Col1 on ClusteredIndexScanDirection(Col1 ASC) DECLARE @date datetime,@i int=0 set @date=GETDATE() while @i<1000000 begin insert into ClusteredIndexScanDirection values (NEWID(),NEWID(),DATEADD(MI,@i,GETDATE())) set @i=@i+1 end set statistics io on SELECT * FROM ClusteredIndexScanDirection WITH (NOLOCK) WHERE Col4 >= '2016-6-1' AND Col4 <= '2016-6-15' ORDER BY Col1 ASC OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY SELECT * FROM ClusteredIndexScanDirection WITH (NOLOCK) WHERE Col4 >= '2016-6-1' AND Col4 <= '2016-6-15' ORDER BY Col1 DESC OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY
20160606再次後記:
A表上的索引大概是這樣的:create index idx_date on A(BusinessDate )這兩個大表join,由於結果集的排序與其中一個主表(也是最大的表)的彙集索引一致一致的話,他就是Forward方式的了,可是,在邏輯上,最近的數據分佈在B樹的右邊,那就是幾乎要遍歷整個表才能查詢出來符合條件數據爲了不這個問題,那就先對A表進行查詢,將結果放入臨時表select * into #A from A where A.BusinessDate>'2016-6-1' and A.BusinessDate<'2016-6-6'而後再在#A上創建相關索引,在跟其餘表join,繞開直接join時走index Forward的方式進行查詢固然實際問題沒這麼簡單,原始查詢20多秒,採用這種方式優化後2s,差很少有十幾倍的提升,效果仍是比較明顯的。