您可以比较以下几种方法。首先,让我们建立一个包含一些虚拟数据的表。我用一堆来自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。结果:
结论
在我的测试中,选择是:
- SQL Server 2012方法(如果我有SQL Server 2012)。
- 如果SQL Server 2012不可用,并且我的日期是连续的,那么我将使用带有日期的递归cte方法。
- 如果1.和2.都不适用,那么即使性能接近,我也会通过古怪的更新进行自联接,只是因为行为已得到记录并得到保证。我不太担心将来的兼容性,因为希望如果怪异的更新失败,它将在我将所有代码都转换为1之后出现。:-)
但是同样,您应该针对您的架构和数据对它们进行测试。由于这是人为设计的,行数相对较少的测试,因此也很可能是放屁的事。我已经用不同的模式和行计数进行了其他测试,并且性能启发式方法也大不相同...这就是为什么我问了那么多跟进原始问题的原因。
更新
我在这里发了更多关于此的博客:
最佳总计方法-已针对SQL Server 2012更新
Day
键,值是连续的吗?