SQLSERVER中的假脱机spool

SQLSERVER中的假脱机spool

我发现网上对于假脱机的解释都非常零散,究竟假脱机是什么?

这几天在家里研究了一下,收集了很多网上的资料

假脱机是中文的翻译,而英文的名字叫做 spool

在徐老师写的《SQLSERVER企业级平台管理实践》里提到了一下假脱机

在SQLSERVER I/O问题的那一节

在性能监视器里,有一个计数器“worktables/sec” :

每秒创建的工作表数。例如,工作表可用于存储查询假脱机(query spool),LOB变量,XML变量,表变量,游标的临时结果

在《剖析SQLServer执行计划》里也提到了假脱机

(13) 有时查询优化器需要在tempdb数据库中建立临时工作表。如果是这样的话

就意味着图形执行计划中有标识成Index Spool, Row Count Spool或者Table Spool的图标。

任何时候,使用到工作表一般都会防碍到性能,因为需要额外的I/O开销来维护这个工作表。

之前本人也写过一篇文章:

写这篇文章的时候当时还是对假脱机一知半解

假脱机在MSDN中的执行计划中的逻辑运算符和物理运算符中提到了几个假脱机相关的运算符(详见本文最后面)

Eager Spool

Lazy Spool

Index Spool (有时候也叫 Nonclustered Index Spool)

Row Count Spool

Spool

Table Spool

Window Spool 

Spool, Table Spool, Index Spool, Window Spool 和 Row Count Spool是物理运算符

Eager Spool 和 Lazy Spool是逻辑运算符

这些运算符描述了假脱机是如何工作的,在这里你需要非常清楚逻辑运算符和物理运算符的区别

MSDN中的解释:

逻辑运算符:逻辑运算符描述了用于处理语句的关系代数操作。 换言之,逻辑运算符从概念上描述了需要执行哪些操作。

物理运算符:物理运算符实施由逻辑运算符描述的操作。 每个物理运算符都是一个执行某项具体操作的对象或例程。

例如,某些物理运算符可访问表、索引或视图中的列或行。 其他物理运算符执行其他操作,如计算、聚合、数据完整性检查或联接。

物理运算符具有与其关联的开销。

注意:窗口假脱机是没有Eager Spool和Lazy Spool之分的,因为他既是逻辑运算符也是物理运算符!!

简单来讲SQLSERVER做某项操作由物理运算符来做,而具体怎样做就由逻辑运算符来决定

打个比方:小明在佛山,想去广州,小明可以选择开汽车去广州,踩自行车去广州,骑摩托车去广州(相当于做某项操作)

小明可以根据当时的路况:

(1)踩自行车:如果道路比较拥堵,踩自行车不用怕,最多的车也能过,他可以选择使劲的踩(Eager Spool)或者慢慢踩(Lazy Spool)

(2)开汽车:如果道路比较畅通,他可以选择开快一点(Eager Spool)或者开慢一点(Lazy Spool)

(3)骑摩托车:如果道路比较拥堵,他可以选择抄小路,然后开快一点(Eager Spool)或者开慢一点(Lazy Spool)

不知道这个比喻大家明白没有,不过本人也找不到更好的比喻~

在图形执行计划中,你会发现Table Spool 有时候会带有  Eager Spool ,有时候有会带有 Lazy Spool

因为Table Spool是物理运算符,Eager Spool和Eager Spool 是逻辑运算符

Table Spool(表假脱机)

SQL脚本如下:

表假脱机 Eager Spool

例子出处:

In this case, it writes the data to a temporary spool, sorts the output of that

and then re-reads that spool in the second branch. 

表假脱机 Lazy Spool

例子出处:

Row Count Spool(行计数假脱机)

SQL脚本如下:

例子出处:

Index Spool (索引假脱机)

Lazy Spool

SQL脚本如下:

例子出处:

Window Spool  (窗口假脱机)

Window Spool 这个执行计划和OVER() 开窗函数息息相关,因为只有OVER()函数才会使用到Window Spool 这个执行计划

大家可以看一下MSDN中对OVER()开窗函数里ROWS选项RANGE选项的解释

ROWS | RANGE
通过指定分区中的起点和终点,进一步限制分区中的行数。 这是通过按照逻辑关联或物理关联对当前行指定某一范围的行实现的。

物理关联通过使用 ROWS 子句实现。

ROWS 子句通过指定当前行之前或之后的固定数目的行,限制分区中的行数。

此外,RANGE 子句通过指定针对当前行中的值的某一范围的值,从逻辑上限制分区中的行数。

基于 ORDER BY 子句中的顺序对之前和之后的行进行定义。

窗口框架“RANGE … CURRENT ROW …”包括在 ORDER BY 表达式中与当前行具有相同值的所有行。

例如,ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 意味着该函数对其操作的行的窗口在大小上是 3 行,以当前行之前(包括当前行)的 2 行开头。

SQL脚本如下:

例子出处:

对上面这些运算符的解释:

假脱机运算符会取出表中的一部分的数据集,将他们存放在tempdb数据库里的临时结构里

这个临时结构一般就是堆表或者非聚集索引,但是有一个物理运算符例外,临时结构是不存放数据的,

他只存放假脱机里保存的数据的行数,而这个物理运算符就是Row Count spool

Index Spool:索引假脱机只有非聚集索引假脱机,没有聚集索引假脱机,结合我以前写的两篇文章,解释一下原因

SQLSERVER当遇到复杂的查询的时候,需要把部分结果集放到tempdb数据库里的非聚集索引页里(说白了就是在tempdb数据库里建立

表的非聚集索引)以加快查找的速度的时候就会用到索引假脱机

例如上面的例子,SQL语句用到了子查询(tb表),SQLSERVER需要把子查询里的结果集(tb表)进行排序然后将结果集放进去

非聚集索引里(对tb表建立非聚集索引),

然后用非聚集索引里的数据和主表(tb a)里的数据进行联接,并输出结果

为什麽不用聚集索引?

SQLSERVER聚集索引与非聚集索引的再次研究(上/下)里说到,非聚集索引和堆表是没有连接在一起的,非聚集索引页面只有指针

指向堆表的数据页,而聚集索引的叶子节点就是数据页,索引页和数据页连接在一起,如果建立聚集索引,就需要将表(tb表)中的数据

放入到tempdb数据库里,这样开销就会很大

或者用下面两张图来描述可能会清楚一点,关键还是要读懂 SQLSERVER聚集索引与非聚集索引的再次研究(上/下)

Table Spool:把表中的数据放进tempdb数据库里

为什麽第一个查询会用到Table Spool?因为CUBE这个数据汇总关键字会将表中的数据进行汇总,汇总的过程比较复杂

把表中的数据放进去tempdb数据库里的工作表(worktable、临时表、堆表)里进行复杂的汇总计算是比较好的

他避免了阻塞,以防止长期锁住表中的数据

Row Count Spool:存放中间结果/表的数据的行数,上面的例子里用于计算表中的数据行数并保存在tempdb数据库的

Row Count Spool里,为后面两表联接选用执行计划提供选择依据

Eager Spool逻辑运算符:一次性将所有数据放入到Spool里

Lazy Spool逻辑运算符:逐次逐次地将数据放入Spool里

在上面的例子里

Tabel Spool Eager Spool

SQLSERVER使用Eager Spool一次性将Sales 表中的数据存放到tempdb数据库的工作表里面,方便快速统计

Row Count Spool

SQLSERVER使用计数器每次读取到一行就加1,这样一次一次地统计表中的行数(这里只是比喻,SQLSERVER内部可能并不是这样统计!)

Window Spools:根据MSDN中的定义,OVER 子句定义查询结果集内的窗口或用户指定的行集。 然后,开窗函数将计算窗口中每一行的值

SQLSERVER将窗口中的结果集放入Spool里,以加快后续操作的速度

对于单独一个窗口来讲:单独一个窗口属于Eager Spool(一次性将结果集放进去窗口里)

对于表中的窗口来讲:属于Lazy Spool ,因为每个窗口把数据存放进去窗口里的速度/顺序不是一致的,逐次逐次地将数据存放进去每个窗口

为什麽需要假脱机?

主要有两个原因:

1:数据需要再次被调用

2:使假脱机数据与源数据保持隔离

第二个原因很容易理解,就像第一个例子中的Tabel Spool那样,需要把表数据放进Tabel Spool里,以方便进行数据汇总,

而不影响原表数据

第一个原因可以再举一个例子

公用表表达式(CTE)

索引假脱机运算符负责把数据一条一条地塞进去tempdb的非聚集索引里,并且是Lazy的,为什麽是Lazy的?

因为刚开始的时候只有一行记录,后来慢慢一条一条数据地从最右边的表假脱机里获取数据

我们还是先分析一下整个执行计划以方便理解,我们可以将整个执行计划拆解为三部分

第一部分 执行计划的右上角

这部分的执行计划只查找到一条记录

他把这条记录放入索引假脱机里

第二部分 UNION ALL

将第一部分的结果和第三部分的结果合并在一起

第三部分 执行计划的右下角

最右边的表假脱机运算符负责把表数据装载入表假脱机里,这个装载过程也是逐条数据装载的

那么,执行计划里的表假脱机和索引假脱机主要有什么用???

表假脱机主要用作公用表表达式里的递归调用

SELECT e.[EmployeeID],e.[ManagerID]
from [managers] m
JOIN [HumanResources].[Employee] e
ON e.[ManagerID]=m.[EmployeeID]

上面的代码是每次递归的时候都需要调用到的,所以SQLSERVER干脆把表数据放到假脱机里的,不用每次都去查找记录了

而索引假脱机是方便外部代码调用公用表表达式的时候不用每次都去计算公用表表达式的结果,直接把公用表表达式的结果

放进去索引假脱机,当SELECT * FROM managers的时候,直接到索引假脱机里取数据就可以了

断定运算符在这里的作用是判断是否超过系统循环次数造成死循环,如果我们加上OPTION (MAXRECURSION 0)

断定运算符就会消失

万圣节问题

网上有两篇文章介绍了这个问题

园子里的这篇文章介绍非常不深入,看了之后还是不明白

simple-talk网站的文章就介绍得非常清晰

在介绍之前先来做一个小实验

下面SQL脚本建立一个非聚集索引表,并且非聚集索引的第一个字段是salary 并且按salary升序排序!!!

我们看一下非聚集索引页

非聚集索引按照Salary字段升序排序

我们用SQL语句update一下小华的Salary

这里是按照非聚集索引的Range Scan读取出结果的:SQLSERVER中的ALLOCATION SCAN和RANGE SCAN

再看一下非聚集索引页面

我们看一下update前和update后非聚集索引页面的变化

可以看到,update之后非聚集索引马上根据非聚集索引键(Salary字段)重新进行升序排序

——————————————————————————————–

使用下面SQL脚本建立测试环境

我们用下面SQL语句看一下聚集索引页面和非聚集索引页面

聚集索引页面

非聚集索引页面

我们update一下Salary等于1的那位员工的工资

再看一下聚集索引页面和非聚集索引页面

聚集索引页面

非聚集索引页面

非聚集索引马上按照非聚集索引键(Salary字段)进行重新排序

这里似乎没有什么问题,我们drop掉Halloween表,并重新建立测试环境

这次我们使用下面update语句,记住一定要加WITH(INDEX=ix_Halloween)

如果我们加了WITH(INDEX=ix_Halloween),SQLSERVER就会走非聚集索引查找

如果我们不加WITH(INDEX=ix_Halloween),SQLSERVER就会走聚集索引扫描

这里不讨论加不加WITH(INDEX=ix_Halloween)的问题

关键我们加WITH(INDEX=ix_Halloween)就是为了让SQLSERVER走非聚集索引

update了之后正常的结果应该是这样的

为什麽会这样?

还记得刚才我们说到了非聚集索引更新了之后马上进行排序吗?

用下面的图来表示应该会比较清楚

SQLSERVER使用Table Spool来解决万圣节问题

先将非聚集索引的数据放进去Table Spool(工作表)里,然后逐行逐行扫描工作表,这样就不会遇到非聚集索引更新后马上进行排序的问题了

使用Table Spool后就能够得到正确结果

为什麽不用Index Spool而用Table Spool?

之前我们说过Index Spool在tempdb数据库里建立临时的非聚集索引,把非聚集索引里的数据

放进去非聚集索引里,那不是会继续遇到万圣节问题???

下面这个SQL语句也是使用了Table Spool来避免万圣节问题

总结一下万圣节问题

update数据的时候,如果update的是非聚集索引的第一个字段(即非聚集索引键)的时候并且走的是非聚集索引扫描/查找

都有可能引起万圣节问题

SQLSERVER的解决方法是把非聚集索引里的数据全部移到Tabel Spool(Eager)里

防止由于更新非聚集索引的非聚集索引键而引起的非聚集索引重新排序,造成数据更新错误的问题

总结

实际上这些假脱机运算符的本质跟临时表和表变量是一样的,都是以空间换时间,以达到性能上的平衡!

文章最后面附上MSDN里的SQLSERVER所有的执行计划(逻辑运算符和物理运算符)

参考文章:

如有不对的地方,欢迎大家拍砖o(∩_∩)o