1. 程式人生 > >Sql Server 聚集索引掃描 Scan Direction的兩種方式------FORWARD 和 BACKWARD

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,差不多有十幾倍的提高,效果還是比較明顯的。