以下に比較できる方法をいくつか示します。最初に、ダミーデータを含むテーブルを設定しましょう。sys.all_columnsからのランダムデータの束をこれに追加しています。まあ、それは一種のランダムです-私は日付が連続していることを保証しています(これは答えの1つにとってのみ重要です)。
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」-提案されたミカエルの回答から盗まれた
- サポートされていない定義済みの動作を約束していないが、非常に人気があると思われる「風変わりな更新」
- "カーソル"
- 新しいウィンドウ機能を使用した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
提案されているように、テストにこれを含めるためのミカエルの答えを盗みます。
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は1パスで計算されます)、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とループの両方を削除し、1つの生の反復を測定するだけです。
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倍しました(トップを50000に変更し、クロス結合として別のテーブルを追加します)。この結果、DBCCコマンドを使用しない1回の反復(時間の都合上):
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
、キーであり、値は連続していますか?