使用GROUP BY进行T-SQL交叉应用

时间:2014-10-09 21:24:28

标签: sql-server performance tsql group-by cross-apply

我是CROSS APPLY的新手,并试图了解其确切工作原理的机制。具体来说,在进行一些测试时,我发现在CROSS APPLY语句中包含GROUP BY子句可以大大提高聚合的性能,但这似乎有点违反直觉。我想让我感到困惑的是准确的操作顺序。

这是我的测试:

declare @cust table (CUSTID int, NAME varchar(30), MaxOrder decimal, TotalAmountSpent decimal, OrderCount int) 
declare @order table (OID int, CUSTID int, AMOUNT decimal)

insert into @cust values (01, 'Fred', 0, 0, 0)
insert into @cust values (02, 'Mary', 0, 0, 0)
insert into @cust values (03, 'Karl', 0, 0, 0)

insert into @order values (20, 01, 6.00)
insert into @order values (21, 03, 10.00)
insert into @order values (22, 03, 20.00)

update @cust
   set MaxOrder = app.MaxOrder, TotalAmountSpent = app.TotalAmountSpent, OrderCount = app.OrderCount
  from @cust c

 cross apply (
               select MAX(AMOUNT) MaxOrder, SUM(AMOUNT) TotalAmountSpent, COUNT(OID) OrderCount 
                 from @order o 
                where c.CUSTID = o.CUSTID 
                group by o.CUSTID
             ) app

select * from @cust

这会产生正确的结果:

CUSTID  NAME    MaxOrder    TotalAmountSpent    OrderCount
1       Fred           6                   6             1
2       Mary           0                   0             0
3       Karl          20                  30             2

注释掉GROUP BY会导致Mary的值被写为NULL:

CUSTID  NAME    MaxOrder    TotalAmountSpent    OrderCount
1       Fred           6                   6             1
2       Mary        NULL                NULL             0
3       Karl          20                  30             2

因此,虽然两个结果集都可以被视为“正确”,但第一种方法仅影响实际相关的行。在更大的数据集上,这似乎可以提高性能。

以下是我对此感到困惑:一般来说,我相信在任何SQL语句中,WHERE子句都将在GROUP BY子句之前处理,不是吗?在这种情况下,SQL Server查询优化器是否知道在左表和右表之间应用WHERE子句之前首先执行GR​​OUP BY?令我感到惊讶的是,以这种方式编写它会导致正确的结果和更好的性能。我们非常感谢对引擎盖下发生的事情的解释。

谢谢!

2 个答案:

答案 0 :(得分:3)

不是异步执行where子句和group by子句,而是查询优化器认为最有效的路径。使用group by查看查询计划,在流聚合和合并连接之前,引入了两种类型,每种类型一种。分组列表在分组时比未分类列表更快聚合 - 需要的比较更少,需要的检查/ IO更少 - 只是分组表达式发生变化的每个时间间隔,它都会设置一个新组并继续流式传输数值英寸

另一方面,如果没有它,使用where子句的应用查询就足以只返回1行,因此不会破坏结果集,因为它是所有聚合函数。如果没有group by,则无需跟踪任何表达式中的更改,只需从where子句条件中提供任何匹配的聚合。

结果是否相同?不完全,但是将简单合并为零比在另一个查询计划中与两个排序相关联的处理成本更简单。

答案 1 :(得分:2)

有趣的行为。严格来说,您的查询不正确 - 如果您没有Mary的订单但仍希望更新其记录,则应使用outer apply代替cross。此外,它可能会更好地处理这个"没有记录" isnull()部分中包含set包装器的方案。

现在,Mary的行中的值不会被零重写 - 它们保持不变,因为apply不会为她返回任何内容。您可以通过更改表初始化来看到这一点:

insert into @cust values (01, 'Fred', -1, -1, -1)
insert into @cust values (02, 'Mary', -1, -1, -1)
insert into @cust values (03, 'Karl', -1, -1, -1)

group by到位时,玛丽的行没有得到零,它仍然拥有所有那些-1。当您尝试使用不返回任何行的查询为标量变量赋值时,它的行为完全相同 - 该变量在此之后仍将保持其先前的值。它是一个记录在案且众所周知的特征。

尽管如此,它仍然非常有趣(至少对我来说),为什么评论group by会如此彻底地改变行为。我们可以通过查看apply子查询的结果来缩小范围,如下所示:

select MAX(AMOUNT) MaxOrder, SUM(AMOUNT) TotalAmountSpent, COUNT(OID) OrderCount
from @order o 
where o.CUSTID = 2;

select MAX(AMOUNT) MaxOrder, SUM(AMOUNT) TotalAmountSpent, COUNT(OID) OrderCount
from @order o 
where o.CUSTID = 2
group by o.CUSTID;

如图所示,指定分组条件可作为附加过滤器使用。这可能是在SQL Server中实现聚合的方式。

编辑:经过一番搜索后,我发现Oracle的工作方式完全相同。所以这似乎是一种标准行为。此外,这里还讨论了这种效果:Count Returning blank instead of 0

简而言之,group by过滤掉不存在的群组,因此当您指定没有销售的客户时,您什么也得不到。但是,如果没有分组,则没有这样的过滤阶段,因此您会收到整个表的聚合 - nullmax的{​​{1}}以及sum的归零。在您的特定示例中,count实际上是不必要的,因为所有返回的列都是聚合(这是非常罕见的)。