Sql Server 汇集索引扫描 Scan Direction的两种方式------FORWARD 和 BACKWARD

最近发现一个分页查询存储过程当中的的一个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,差很少有十几倍的提升,效果仍是比较明显的。

相关文章
相关标签/搜索