运行总数与计数?


34

如标题所示,我需要一些帮助来使T-SQL的运行状况更全面。问题是我需要做的总和是一个计数的总和:

sum(count (distinct (customers))) 

假设我只运行计数,结果将是:

Day | CountCustomers
----------------------
5/1  |      1
5/2  |      0
5/3  |      5

我需要输出的总和为:

Day | RunningTotalCustomers
----------------------
5/1  |      1
5/2  |      1
5/3  |      6

在使用该coalesce方法之前,我已经完成了总计操作,但从未进行过计数。现在我不知道该怎么做了。


2
请使用哪个版本的SQL Server?您能否共享数据的范围-我们是在谈论1000行,一百万,十亿吗?真的只是这两栏,还是您为我们简化了架构?最后,是Day键,值是连续的吗?
亚伦·伯特兰

我做了一个关于运行总计的全面博客(Quirky更新vs混合递归CTE vs游标):ienablemuch.com/2012/05/…我没有包括使用基于纯集合的方法的运行总计,性能没什么好需要:sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…–
Michael Buen

Answers:


53

您可以比较以下几种方法。首先,让我们建立一个包含一些虚拟数据的表。我用一堆来自sys.all_columns的随机数据填充它。好吧,这是随机的-我确保日期是连续的(这实际上仅对其中一个答案很重要)。

CREATE TABLE dbo.Hits(Day SMALLDATETIME, CustomerID INT);

CREATE CLUSTERED INDEX x ON dbo.Hits([Day]);

INSERT dbo.Hits SELECT TOP (5000) DATEADD(DAY, r, '20120501'),
  COALESCE(ASCII(SUBSTRING(name, s, 1)), 86)
FROM (SELECT name, r = ROW_NUMBER() OVER (ORDER BY name)/10,
       s = CONVERT(INT, RIGHT(CONVERT(VARCHAR(20), [object_id]), 1))
FROM sys.all_columns) AS x;

SELECT 
  Earliest_Day   = MIN([Day]), 
  Latest_Day     = MAX([Day]), 
  Unique_Days    = DATEDIFF(DAY, MIN([Day]), MAX([Day])) + 1, 
  Total_Rows     = COUNT(*)
FROM dbo.Hits;

结果:

Earliest_Day         Latest_Day           Unique_Days  Total_Days
-------------------  -------------------  -----------  ----------
2012-05-01 00:00:00  2013-09-13 00:00:00  501          5000

数据看起来像这样(5000行)-但是在您的系统上看起来会略有不同,具体取决于版本和内部版本号:

Day                  CustomerID
-------------------  ---
2012-05-01 00:00:00  95
2012-05-01 00:00:00  97
2012-05-01 00:00:00  97
2012-05-01 00:00:00  117
2012-05-01 00:00:00  100
...
2012-05-02 00:00:00  110
2012-05-02 00:00:00  110
2012-05-02 00:00:00  95
...

运行总计结果应如下所示(501行):

Day                  c   rt
-------------------  --  --
2012-05-01 00:00:00  6   6
2012-05-02 00:00:00  5   11
2012-05-03 00:00:00  4   15
2012-05-04 00:00:00  7   22
2012-05-05 00:00:00  6   28
...

所以我要比较的方法是:

  • “自我加入”-基于集合的纯粹方法
  • “带日期的递归CTE”-这取决于连续的日期(无间隔)
  • “具有row_number的递归CTE”-与上述类似,但速度较慢,取决于ROW_NUMBER
  • “带有#temp表的递归CTE”-根据建议从Mikael的答案中被盗
  • 尽管不被支持且没有希望的定义行为,但“流行更新”似乎非常流行
  • “光标”
  • SQL Server 2012使用新的窗口功能

自我加入

人们警告您不要游标时,这就是人们会告诉您这样做的方式,因为“基于集合的总是更快”。在最近的一些实验中,我发现光标超出了该解决方案的速度。

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], g.c, rt = SUM(g2.c)
  FROM g INNER JOIN g AS g2
  ON g.[Day] >= g2.[Day]
GROUP BY g.[Day], g.c
ORDER BY g.[Day];

带有日期的递归CTE

提醒-这取决于连续的日期(无间隔),最多10000个递归级别,并且您知道自己感兴趣的范围的开始日期(设置锚点)。当然,您可以使用子查询来动态设置锚,但是我想保持简单。

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], c, rt = c
        FROM g
        WHERE [Day] = '20120501'
    UNION ALL
    SELECT g.[Day], g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.[Day] = DATEADD(DAY, 1, x.[Day])
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

具有row_number的递归cte

Row_number的计算在这里有点昂贵。同样,这支持10000的最大递归级别,但是您不需要分配锚点。

;WITH g AS 
(
  SELECT [Day], rn = ROW_NUMBER() OVER (ORDER BY DAY), 
    c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], rn, c, rt = c
        FROM g
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

带有临时表的递归CTE

按照建议,从Mikael的答案中窃取,以将其包括在测试中。

CREATE TABLE #Hits
(
  rn INT PRIMARY KEY,
  c INT,
  [Day] SMALLDATETIME
);

INSERT INTO #Hits (rn, c, Day)
SELECT ROW_NUMBER() OVER (ORDER BY DAY),
       COUNT(DISTINCT CustomerID),
       [Day]
FROM dbo.Hits
GROUP BY [Day];

WITH x AS
(
    SELECT [Day], rn, c, rt = c
        FROM #Hits as c
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN #Hits as g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

DROP TABLE #Hits;

古怪的更新

再次,我仅出于完整性考虑而包括在内。我个人不会依赖此解决方案,因为正如我在另一个答案中提到的那样,这种方法根本无法保证会起作用,并且在将来的SQL Server版本中可能会完全中断。(我正在尽力使SQL Server遵循我想要的顺序,并使用提示选择索引。)

CREATE TABLE #x([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x([Day]);

INSERT #x([Day], c) 
    SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt1 INT;
SET @rt1 = 0;

UPDATE #x
SET @rt1 = rt = @rt1 + c
FROM #x WITH (INDEX = x);

SELECT [Day], c, rt FROM #x ORDER BY [Day];

DROP TABLE #x;

光标

“当心,这里有光标!光标是邪恶的!您应该不惜一切代价避免使用光标!” 不,这不是我在说话,这只是我经常听到的东西。与流行观点相反,在某些情况下使用游标是合适的。

CREATE TABLE #x2([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x2([Day]);

INSERT #x2([Day], c) 
    SELECT [Day], COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt2 INT, @d SMALLDATETIME, @c INT;
SET @rt2 = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT [Day], c FROM #x2 ORDER BY [Day];

OPEN c;

FETCH NEXT FROM c INTO @d, @c;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt2 = @rt2 + @c;
  UPDATE #x2 SET rt = @rt2 WHERE [Day] = @d;
  FETCH NEXT FROM c INTO @d, @c;
END

SELECT [Day], c, rt FROM #x2 ORDER BY [Day];

DROP TABLE #x2;

SQL Server 2012

如果您使用的是SQL Server的最新版本,则窗口功能的增强使我们能够轻松地计算运行总计,而无需花费大量的自我连接费用(SUM是一次性计算的),CTE的复杂性(包括要求)连续的行,以获得更好的CTE性能),不受支持的新奇更新和禁止使用的游标。只是要警惕使用RANGE和之间的区别ROWS,或者根本不指定-仅ROWS避免使用磁盘假脱机,否则将严重影响性能。

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], c, 
  rt = SUM(c) OVER (ORDER BY [Day] ROWS UNBOUNDED PRECEDING)
FROM g
ORDER BY g.[Day];

性能比较

我采用了每种方法,并使用以下方法将其包装成批:

SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();

以下是总持续时间的结果,以毫秒为单位(请记住,每次也包含DBCC命令):

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1296 ms   1357 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1655 ms   1516 ms
recursive cte with row_number   19747 ms  19630 ms
recursive cte with #temp table   1624 ms   1329 ms
quirky update                     880 ms   1030 ms -- non-SQL 2012 winner
cursor                           1962 ms   1850 ms
SQL Server 2012                   847 ms    917 ms -- winner if SQL 2012 available

我再次使用了DBCC命令:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1272 ms   1309 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1247 ms   1593 ms
recursive cte with row_number   18646 ms  18803 ms
recursive cte with #temp table   1340 ms   1564 ms
quirky update                    1024 ms   1116 ms -- non-SQL 2012 winner
cursor                           1969 ms   1835 ms
SQL Server 2012                   600 ms    569 ms -- winner if SQL 2012 available

删除DBCC和循环,仅测量一个原始迭代:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                         313 ms    242 ms
recursive cte with dates          217 ms    217 ms
recursive cte with row_number    2114 ms   1976 ms
recursive cte with #temp table     83 ms    116 ms -- "supported" non-SQL 2012 winner
quirky update                      86 ms     85 ms -- non-SQL 2012 winner
cursor                           1060 ms    983 ms
SQL Server 2012                    68 ms     40 ms -- winner if SQL 2012 available

最后,我将源表中的行数乘以10(将top更改为50000并添加另一个表作为交叉联接)。结果是,一次没有DBCC命令的迭代(仅出于时间考虑):

method                           run 1      run 2
-----------------------------    --------   --------
self-join                         2401 ms    2520 ms
recursive cte with dates           442 ms     473 ms
recursive cte with row_number   144548 ms  147716 ms
recursive cte with #temp table     245 ms     236 ms -- "supported" non-SQL 2012 winner
quirky update                      150 ms     148 ms -- non-SQL 2012 winner
cursor                            1453 ms    1395 ms
SQL Server 2012                    131 ms     133 ms -- winner

我只测量了持续时间-我将其作为练习供读者比较它们的数据上的这些方法,比较其他可能重要的指标(或可能因其架构/数据而异)。在从此答案中得出任何结论之前,您将需要根据数据和架构对其进行测试……随着行数的增加,这些结果几乎肯定会发生变化。


演示

我添加了一个sqlfiddle。结果:

在此处输入图片说明


结论

在我的测试中,选择是:

  1. SQL Server 2012方法(如果我有SQL Server 2012)。
  2. 如果SQL Server 2012不可用,并且我的日期是连续的,那么我将使用带有日期的递归cte方法。
  3. 如果1.和2.都不适用,那么即使性能接近,我也会通过古怪的更新进行自联接,只是因为行为已得到记录并得到保证。我不太担心将来的兼容性,因为希望如果怪异的更新失败,它将在我将所有代码都转换为1之后出现。:-)

但是同样,您应该针对您的架构和数据对它们进行测试。由于这是人为设计的,行数相对较少的测试,因此也很可能是放屁的事。我已经用不同的模式和行计数进行了其他测试,并且性能启发式方法也大不相同...这就是为什么我问了那么多跟进原始问题的原因。


更新

我在这里发了更多关于此的博客:

最佳总计方法-已针对SQL Server 2012更新


1

显然,这是最佳解决方案

DECLARE @dailyCustomers TABLE (day smalldatetime, CountCustomers int, RunningTotal int)

DECLARE @RunningTotal int

SET @RunningTotal = 0

INSERT INTO @dailyCustomers 
SELECT day, CountCustomers, null
FROM Sales
ORDER BY day

UPDATE @dailyCustomers
SET @RunningTotal = RunningTotal = @RunningTotal + CountCustomers
FROM @dailyCustomers

SELECT * FROM @dailyCustomers

有没有实现临时表的任何想法(我的proc已经根据需要强制通过多个临时表的值,所以我试图找到一种避免使用另一个临时表的方法)?如果没有,我将使用此方法。我认为它将起作用

也可以使用自联接或嵌套子查询来完成,但是这些选项的执行效果不尽如人意。而且,无论如何,使用这些具有假脱机或工作表的替代方法,您仍然可能会遇到tempdb。

3
请注意,不能保证此“古怪的更新”方法可以正常工作-不支持此语法,并且未定义其行为,并且它可能会在将来的版本,修补程序或Service Pack中中断。因此,虽然是的,但它比某些受支持的替代品要快,但要付出潜在的未来兼容性成本。
亚伦·伯特兰

6
Jeff Moden在某处写了很多关于此方法的警告。例如,您应该具有聚集索引day
马丁·史密斯

2
@MartinSmith这是sqlservercentral.com上的一篇非常大的文章(转到“作者”页面,并在快速更新中找到他的文章)。
Fabricio Araujo 2012年

-2

这只是另一种方式,成本高昂,但与版本无关。它不使用临时表或变量。

select T.dday, T.CustomersByDay + 
    (select count(A.customer) from NewCustomersByDate A 
      where A.dday < T.dday) as TotalCustomerTillNow 
from (select dday, count(customer) as CustomersByDay 
        from NewCustomersByDate group by dday) T 

2
那不好,那很慢。即使您只有100行,它也会在表之间进行5,050次的乒乓读取。200行,是20,100次。仅使用1,000行,读取sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…的
Michael Buen

发布此消息后,我看到了指向您博客的链接,现在我看到这是一个非常糟糕的主意,谢谢!
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.