4

我是 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 子句之前先执行 GROUP BY?令我惊讶的是,以这种方式编写它会导致正确的结果和更好的性能。非常感谢对引擎盖下到底发生了什么的解释。

谢谢!

4

2 回答 2

3

这与异步执行的 where 子句和 group by 子句无关,而是查询优化器认为最有效的路径。查看带有 group by 的查询计划,在流聚合和合并连接之前引入了两种排序,每个表一种。与未排序的列表相比,分组时排序列表的聚合速度更快 - 需要的比较和检查/IO 更少 - 只是分组表达式更改的每个间隔,它都会设置一个新组并继续流式传输您的数值在。

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

结果是一样的吗?不完全是,但是做一个简单的归零比与另一个查询计划中的两个排序相关的处理成本更简单。

于 2014-10-09T22:58:31.583 回答
2

有趣的行为。严格来说,您的查询不正确 - 如果您没有 Mary 的订单但仍想更新她的记录,您应该使用outer apply而不是cross. isnull()此外,在部件中使用包装器处理这种“无记录”场景可能会更好set

现在,玛丽所在行中的值不会用零重写 - 它们保持不变,因为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而不是0

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

于 2014-10-10T04:55:00.993 回答