カウントで実行中の合計?


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行、100万、10億について話していますか?本当にこれらの2つの列だけですか、それともスキーマを単純化しましたか?最後にDay、キーであり、値は連続していますか?
アーロンバートランド

ランニングトータル(風変わりな更新vsハイブリッド再帰CTE vsカーソル)に関する包括的なブログを作成しました:ienablemuch.com/2012/05/…純粋なセットベースのアプローチを使用するランニングトータルを含めませんでした。パフォーマンスは何もありません希望:sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…–
マイケル・

回答:


53

以下に比較できる方法をいくつか示します。最初に、ダミーデータを含むテーブルを設定しましょう。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のための連続した行)、サポートされていない風変わりな更新、禁止されたカーソル。ただ、使用の違いを警戒するRANGEROWS、またはまったく指定しない-だけで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を追加しました。結果:

ここに画像の説明を入力してください


結論

私のテストでは、選択は次のようになります。

  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

一時テーブルを実装しないアイデア(私のプロシージャは、必要に応じていくつかの一時テーブルに値を既に強制しているので、別の一時テーブルの使用を避ける方法を見つけようとしています)?そうでない場合は、この方法を使用します。私はそれがうまくいくと思う

また、自己結合またはネストされたサブクエリを使用して実行することもできますが、これらのオプションはほぼ同様に機能しません。また、スプーリングまたはワークテーブルを使用してこれらの代替手段を使用して、いずれにしてもtempdbにアクセスする可能性があります。

3
この「風変わりな更新」メソッドが機能することが保証されていないことに注意してください-この構文はサポートされておらず、その動作は未定義であり、将来のバージョン、ホットフィックス、またはサービスパックで壊れる可能性があります。そのため、サポートされている他の選択肢よりも高速ですが、将来の互換性コストが発生する可能性があります。
アーロンバートランド

6
このアプローチには、Jeff Modenがどこかに書いた多くの警告があります。dayたとえば、クラスタ化インデックスが必要です。
マーティンスミス

2
それはある@MartinSmith VERY BIG sqlservercentral.comの記事(著者ページに移動し、quirckアップデートで彼の記事を見つけます。)。
ファブリシオアラウージョ

-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行で、それは読み500500に指数関数的にジャンプsqlblog.com/blogs/adam_machanic/archive/2006/07/12/...を
マイケル・ブエン

これを投稿した後、あなたのブログへのリンクを見ました、今、これは非常に悪い考えだと思います、ありがとう!
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.