最近發現一個分頁查詢存儲過程中的的一個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 @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,差不多有十幾倍的提高,效果還是比較明顯的。
