【问题标题】:T-SQL CROSS APPLY with GROUP BYT-SQL CROSS APPLY 与 GROUP BY
【发布时间】:2014-12-04 22:57:51
【问题描述】:

我是 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?令我惊讶的是,以这种方式编写它会导致正确的结果和更好的性能。非常感谢您解释到底发生了什么。

谢谢!

【问题讨论】:

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


    【解决方案1】:

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

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

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

    【讨论】:

    • 我还不能投票,但这很有帮助,谢谢。
    【解决方案2】:

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

    现在,玛丽所在行中的值不会被零重写 - 它们保持不变,因为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,Mary 的行不会得到零,它仍然有所有那些 -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 会过滤掉不存在的组,因此当您指定没有销售的客户时,您什么也得不到。但是,如果没有分组,则没有这样的过滤阶段,因此您会收到整个表的聚合 - nulls 用于 max 和 sum,而零用于 count。在您的特定示例中,group by 实际上是不必要的,因为所有返回的列都是聚合的(这非常罕见)。

    【讨论】:

    • 在我最初的测试中,我确实将 OUTER APPLY 作为主查询的一部分(没有更新语句),因为正如您所提到的,这将是自然正确的方法。然而,那是我第一次注意到更大数据集的性能问题。所以,这导致我尝试更新语句,以及使用 GROUP BY 的 CROSS。对我来说,它似乎并不直观,它会起作用,但我已经一次又一次地检查结果,它似乎确实产生了正确的聚合,并且,正如你所描述的,它充当了一个额外的过滤器,可以提高性能。 (还没有足够的代表来投票,但谢谢!)
    • 另外,我使用的是 SQL Server 2008 R2 版。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-11
    • 2010-09-30
    • 2011-06-21
    • 2018-04-21
    • 2014-02-21
    相关资源
    最近更新 更多