前言:本系列文章不知不觉已经到了第十篇了。有朋友一直坚持看本系列,非常感谢。有朋友问我,看是看了,貌似用不着啊。到现在为止,确实用不上,但是,这是明白查询化优化器的必经之路,也是学会查看执行计划的必备知识,更是以后性能调优的基本技能,厚积薄发。
相信对于聚合操作,大家应该是非常的熟悉了。在数据库中,我们常常采用聚合操作来对一批数据进行处理,例如SUM,AVG or MAX,Group By等。我们查询中的聚合操作,属于逻辑操作,在数据库底层,这些逻辑的聚合操作是有两类不同的物理操作来实现的:Stream Aggregate 和 Hash Aggregate。
在真正开始讲述这两个物理操作之前,我这里首先介绍一下排序(Sort)和哈希(Hash)。这两个操作非常重要,例如,如果要进行Stream Aggregate操作,那么就要先保证数据是排序了的。排序的操作是非常消耗内存的,如果内存不足,就会使用tempdb数据来存放数据,从而使得磁盘的读写操作变多和cpu的使用变高。另外,对于没有排序的数据,在聚合的时候,就会采用哈希表的形式,关于这一点,我们在后面讲述Join的时候会进一步的讲述。
总是记住一点就是:如果数据是排序的了,那么聚合的物理操作就会选择Stream Aggregate,那么Join的物理操作就会使用Merge Join;如果数据没有排序,那么聚合就会选项Hash Aggregate,Join就会选择Hash Join。如果我们想在在没有排序的数据上面使用“在排序数据上面才有的操作”,那么数据就会被先排序(被我们或者被数据库)。
Stream Aggregate
还是像之前一样,我们通过一个例子来说明,如下:
执行计划如下:
为了理解上面的执行计划,这里为大家普及一个概念:如果一个查询只使用了聚合函数,同时没有使用Group By,那么这个聚合就称为scalar 聚合,就是说,这个聚合只返回一个值,那么这个聚合的物理操作总是用Stream Aggregate来实现的。对于这个“scalar”的理解,大家可以联想一下ADO.NET中的sqlCommand的一个操作:ExecuteScalar(),返回一个值。
上面的执行计划中使用了Stream Aggregate操作吗,为了更好的查看相关的信息,我们来看看这个查询的文本的执行计划,运行如下查询:
文本执行计划如下:
大家可以看到:在AVG聚合函数执行的时候,Stream Aggregate操作使用了Count和SUM聚合函数,把这两个聚合函数的结果分别放在Expr1004
和Expr1005中,Compute Scalar操作验证做了一个除数是否为零的操作,之后就进行一个转换数据类型的操作,最后就执行运算。
我们再看一个例子,如下:
通过查看执行计划,发现有点怪,如下:
发现没有Sort操作,为什么?
原来SalesOrderID是SalesOrderDetail的聚集索引,也就是说,数据已经是排序的了,所以这里没有Sort操作,并且SUM的聚合操作选用了Stream Aggregate实现。
看到这里,大家心里可能就想了:怎么都是Stream Aggregate?什么意思?看到现在没有什么感觉。
下面我们就来看看 hash Aggregate,看完了之后,两者结合理解,就差不多了。
Hash Aggregate
查询优化器在一个很大的并且数据没有排序的表上面为聚合操作选择Hash Aggregate。我们来看下面的一个查询,如下:
执行计划如下:
在上面的查询中需要根据ContactID来对整个表的数据进行分组的统计,又因为这个表中有3万多条数据,所以对于大量无序数据的一些操作,基本都是采用在内存中建立哈希表的方式来处理。后面我们讲到的Hash Join也是这样的。
下面,我们再来看另外一个查询,如下:
执行计划如下:
大家看这里,应该会非常的纳闷:这个查询和刚刚之前的一个查询差不多,并且ProductLine也不是聚集索引,为什么这里采用了Stream Aggregate,而之前的那个采用了Hash Aggregate?
通过查看Product表本身,我发现:表中的数据只有506条,也就是说,这是一个小表,热查询优化器认为对于这个查询把数据先排序,然后实现Stream Aggregate的成本比Hash Aggregate的成本低。
说的这里,也许大家也想起了一件事情:有时候,我们在一个小表上面建立索引,但是在真正执行的时候,我们发现查询优化器没有使用这个索引,因为它认为做整表扫描比使用索引更加高效。所以这里,首先进行了排序,在执行计划中出现了一个Sort操作,而后执行计划中选择了Stream Aggregate。
下面,我们来做一些操作验证我们之前的一些说法:无序的大表上面的聚合操作会使用Hash,那么有序的大表就会使用Stream Aggregate。
下面,我们在SalesOrderHeader表上面建立一个索引,如下:
执行计划如下:
果然,因为在ContactID建立了索引,数据就进行了一定程度的排序。
此时,我们可以进一步的想:如何我们直接在查询语句中明确的使用排序操作,结果是不是也会使用Stream Aggregate呢,查询如下:
查看执行计划,如下:
看到这里,也许大家又要郁闷了,为什么会这样?
其实,此时查询优化器会在“先Sort,然后Stream Aggregate”和“先Hash Aggregate,然后
Sort”这两个之前进行成本的计算,选择成本最小的那个。
所以,可以看出“没有绝对的标准答案,一定都是视情况而定”(中国的教育从小告诉我们任何时候一定有个标准的肯定答案,此时很多时候不是这样的,特别是性能优化的时候)。