子查询或 leftjoin with group by 哪个更快?

Posted

技术标签:

【中文标题】子查询或 leftjoin with group by 哪个更快?【英文标题】:subquery or leftjoin with group by which one is faster? 【发布时间】:2011-11-13 12:35:39 【问题描述】:

我必须在我的应用程序中使用总计列显示运行总计..​​.所以我使用以下查询来查找运行总计..​​.我发现两者都按我的需要工作。在一个中,我使用了左联接和 group by,在另一个中,我使用了子查询。

现在我的问题是,当我的数据每天增长数千时,哪一种更快,如果数据将限制在 1000 或 2000 行范围内,那么哪一种更好……以及任何其他比这些更快的方法两个????

declare @tmp table(ind int identity(1,1),col1 int)
insert into @tmp
select 2
union
select 4
union
select 7
union 

select 5
union
select 8
union 
select 10



SELECT t1.col1,sum( t2.col1)
FROM @tmp AS t1 LEFT JOIN @tmp t2 ON t1.ind>=t2.ind
group by t1.ind,t1.col1


select t1.col1,(select sum(col1) from  @tmp as t2 where t2.ind<=t1.ind)
from @tmp as t1

【问题讨论】:

请用 SQL 供应商名称标记您的问题。对于 Oracle、SQL-server、mysql 等,答案会有所不同。 我已经为 MS sql server 2005 做了这个 您提供的 2 个答案没有给出相同的结果。将 t2.ind @t-clausen.dk 谢谢先生,但仍然希望任何人都能在所有条件下给出最佳答案。 【参考方案1】:

Itzik Ben Gan 的this document 是关于在 SQL Server 中计算运行总数的一个很好的资源,该资源作为他的活动的一部分提交给 SQL Server 团队,以进一步扩展其最初的 SQL Server 2005 实施中的OVER 子句.在其中他展示了一旦​​进入数万行游标如何执行基于集合的解决方案。 SQL Server 2012 确实扩展了OVER 子句,使这种查询变得更加容易。

SELECT col1,
       SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM   @tmp 

因为您使用的是 SQL Server 2005,但是您无法使用它。

Adam Machanic shows here 如何使用 CLR 来提高标准 TSQL 游标的性能。

对于这个表定义

CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)

我在使用 ALLOW_SNAPSHOT_ISOLATION ON 的数据库中创建了包含 2,000 行和 10,000 行的表,其中一个设置为关闭(原因是我最初的结果是在一个设置为结果)。

所有表的聚集索引只有 1 个根页。每个叶子页面的数量如下所示。

+-------------------------------+-----------+------------+
|                               | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF  |         5 |         22 |
| ALLOW_SNAPSHOT_ISOLATION ON   |         8 |         39 |
+-------------------------------+-----------+------------+

我测试了以下案例(链接显示执行计划)

    Left Join and Group By 相关子查询2000 row plan,10000 row plan CTE from Mikael's (updated) answer CTE below

包含额外的 CTE 选项的原因是为了提供一个 CTE 解决方案,如果不能保证 ind 列是连续的,它仍然可以工作。

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;

WITH    RecursiveCTE
AS      (
        SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
        FROM RunningTotals
        ORDER BY ind
        UNION   ALL
        SELECT  R.ind, R.col1, R.Total
        FROM    (
                SELECT  T.*,
                        T.col1 + Total AS Total,
                        rn = ROW_NUMBER() OVER (ORDER BY T.ind)
                FROM    RunningTotals T
                JOIN    RecursiveCTE R
                        ON  R.ind < T.ind
                ) R
        WHERE   R.rn = 1
        )
SELECT  @col1 =col1, @sumcol1=Total
FROM    RecursiveCTE
OPTION  (MAXRECURSION 0);

所有查询都添加了CAST(col1 AS BIGINT),以避免在运行时出现溢出错误。此外,对于所有这些,我将结果分配给上述变量,以消除在考虑中发送回结果所花费的时间。

结果

+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
|                  |          |        |          Base Table        |         Work Table         |     Time        |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
|                  | Snapshot | Rows   | Scan count | logical reads | Scan count | logical reads | cpu   | elapsed |
| Group By         | On       | 2,000  | 2001       | 12709         |            |               | 1469  | 1250    |
|                  | On       | 10,000 | 10001      | 216678        |            |               | 30906 | 30963   |
|                  | Off      | 2,000  | 2001       | 9251          |            |               | 1140  | 1160    |
|                  | Off      | 10,000 | 10001      | 130089        |            |               | 29906 | 28306   |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query        | On       | 2,000  | 2001       | 12709         |            |               | 844   | 823     |
|                  | On       | 10,000 | 2          | 82            | 10000      | 165025        | 24672 | 24535   |
|                  | Off      | 2,000  | 2001       | 9251          |            |               | 766   | 999     |
|                  | Off      | 10,000 | 2          | 48            | 10000      | 165025        | 25188 | 23880   |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps      | On       | 2,000  | 0          | 4002          | 2          | 12001         | 78    | 101     |
|                  | On       | 10,000 | 0          | 20002         | 2          | 60001         | 344   | 342     |
|                  | Off      | 2,000  | 0          | 4002          | 2          | 12001         | 62    | 253     |
|                  | Off      | 10,000 | 0          | 20002         | 2          | 60001         | 281   | 326     |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On       | 2,000  | 2001       | 4009          | 2          | 12001         | 47    | 75      |
|                  | On       | 10,000 | 10001      | 20040         | 2          | 60001         | 312   | 413     |
|                  | Off      | 2,000  | 2001       | 4006          | 2          | 12001         | 94    | 90      |
|                  | Off      | 10,000 | 10001      | 20023         | 2          | 60001         | 313   | 349     |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+

相关子查询和GROUP BY 版本都使用由RunningTotals 表(T1) 上的聚集索引扫描驱动的“三角形”嵌套循环连接,并且对于该扫描返回的每一行,回溯到表 (T2) 在 T2.ind&lt;=T1.ind 上自行加入。

这意味着重复处理相同的行。当T1.ind=1000 行被处理时,自连接检索所有行并将其与ind &lt;= 1000 相加,然后对于T1.ind=1001 的下一行,再次检索相同的1000 行再次并与一个相加附加行等等。

对于 2,000 行的表,此类操作的总数为 2,001,000,对于 10k 行,通常为 50,005,000 或更多 (n² + n) / 2,这显然呈指数增长。

在 2,000 行的情况下,GROUP BY 和子查询版本之间的主要区别在于前者在连接之后具有流聚合,因此有三列馈入其中(T1.indT2.col1、@987654353 @) 和 T1.indGROUP BY 属性,而后者被计算为标量聚合,在连接之前使用流聚合,只有 T2.col1 馈入其中并且根本没有设置 GROUP BY 属性。可以看出,这种更简单的安排在减少 CPU 时间方面具有明显的好处。

对于 10,000 行的情况,子查询计划存在额外差异。它添加了一个eager spool,它将所有ind,cast(col1 as bigint) 值复制到tempdb。在快照隔离的情况下,它比聚集索引结构更紧凑,最终效果是减少了大约 25% 的读取次数(因为基表为版本信息保留了相当多的空白空间),当此选项关闭时,它会变得不那么紧凑(可能是由于bigintint 的差异)和更多的读取结果。这减少了子查询和按版本分组之间的差距,但子查询仍然获胜。

然而,明显的赢家是递归 CTE。对于“无间隙”版本,从基表读取的逻辑现在是 2 x (n + 1),反映了 n 索引搜索 2 级索引以检索所有行加上末尾的附加行,该行不返回任何内容并终止递归.然而,这仍然意味着要处理 22 页表需要 20,002 次读取!

递归 CTE 版本的逻辑工作表读取非常高。每个源行似乎可以读取 6 个工作表。这些来自存储前一行输出的索引假脱机,然后在下一次迭代中再次读取(Umachandar Jayachandran here 对此进行了很好的解释)。尽管数量众多,但它仍然是表现最好的。

【讨论】:

【参考方案2】:

我想你会发现递归 CTE 快一点。

;with C as
(
  select t.ind,
         t.col1,
         t.col1 as Total
  from @tmp as t
  where t.ind = 1
  union all
  select t.ind,
         t.col1,
         C.Total + t.col1 as Total
  from @tmp as t
    inner join C
      on C.ind + 1 = t.ind
)
select C.col1,
       C.Total
from C

任何其他更快的方法

是的,有。如果您正在寻找出色的性能,您应该在一个简单的选择中提取您的数据,并在您进行演示时在客户端上进行运行总计计算。

【讨论】:

@Eriksson 不错的一位先生,但我从数据库的角度询问出色的性能。谢谢先生。 您需要将连接条件切换为C.ind+1 = t.ind 以使递归部分可分割。 我也刚刚想到,这假设id 序列中没有间隙。我的答案有一个适用于空白的版本。 @Martin - 我知道。您不太可能希望对整个表执行此操作(无 where 子句)并且标识是完整的,除非您在每次运行时都这样做。该顺序也很可能与身份顺序不同。如果您绝对需要在服务器上进行计算,您可以使用带有新主键 int 列的临时表,并用您需要求和的行填充临时表。然后就可以使用 CTE 版本了。另一方面,有了那个临时表,你就可以进行古怪的更新了。 @Mikael - 我的答案中处理差距的版本仅比具有平等寻求的版本效率略低。它仍然寻找索引的正确部分并返回前 1 行。可以肯定的是,尽管对于大量行,游标会比我目前介绍的所有游标更有效。【参考方案3】:

你的问题不是很精确,所以这里有一些应该回答的一般规则。

添加索引。在您过于简化的示例中,它将位于 col1。 使用EXPLAIN 比较查询。这将为您提供有关较大数据会发生什么的提示。 测试(真实)数据并优化您的服务器。查询时间将取决于许多参数。例如,您的数据是否适合服务器的内存?还是您的缓冲区配置足够大? 使用缓存来转移来自数据库服务器的查询。Memcached 是最常用的内存应用级缓存,但其他缓存存在于每个级别。

【讨论】:

以上是关于子查询或 leftjoin with group by 哪个更快?的主要内容,如果未能解决你的问题,请参考以下文章

SQL - 使用 GROUP BY 获取子查询子集中或连接中的最新记录

带有子查询的 Oracle 数据透视

Ecto 子查询中的 SQL WITH AS 语句

如何在没有算术计数的SQL子查询中使用GROUP BY

MySQL 查询优化 Group By with Max

如何在一个查询中的多个子查询上正确使用多个 group_concats 而没有区别?