Sql Server 聚集索引掃描 Scan Direction的兩種方式------FORWARD 和 BACKWARD
最近發現一個分頁查詢儲存過程中的的一個SQL語句,當聚集索引列的排序方式不同的時候,效率差別達到數十倍,讓我感到非常吃驚
由此引發出來分頁查詢的情況下對大表做Clustered Scan的時候,
不同情況下會選擇FORWARD 或者 BACKWARD差別,以及建立聚集索引時,選擇索引列的排序方式的一些思考
廢話不多,上程式碼
先建立一張測試表,在Col1上建立聚集索引,寫入100W條資料
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 和 BACKWARD
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的方式是FORWARD
查詢條件一樣,分頁情況下,排序方式不一樣,效能上有麼有差別?肯定有,太明顯了,如果沒有,本文也就沒有什麼意義了
如圖是上述兩種查詢方式在我本機的測試結果,同樣是前100條資料,因為排序方式不同,其代價也是不同的
邏輯讀,一個是2327,一個是9978次,差別不小吧,在實際場景中,這個差別是非常非常大的,大到足以超乎你想想
對FORWARD和BACKWARD有一個直觀的感受之後,來說說這兩者的區別
如果瞭解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 @[email protected]+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,差不多有十幾倍的提高,效果還是比較明顯的。