このサイトの最初のブログ投稿は、2012年7月にさかのぼり、現在の合計の最善のアプローチについて説明しました。それ以来、現在の合計がより複雑な場合、特に複数のエンティティの現在の合計を計算する必要がある場合、たとえば各顧客の注文について、どのように問題に取り組むかを何度も尋ねられてきました。
元の例では、都市がスピード違反の切符を発行するという架空のケースを使用しました。累計は、(チケットが誰に発行されたか、またはいくらであったかに関係なく)1日あたりのスピード違反切符の数の現在のカウントを単純に集計して保持していました。より複雑な(しかし実用的な)例は、運転免許証ごとにグループ化された、スピード違反の切符の現在の合計値を1日あたりで集計することです。次の表を想像してみましょう:
CREATE TABLE dbo.SpeedingTickets ( IncidentID INT IDENTITY(1,1) PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL ); CREATE UNIQUE INDEX x ON dbo.SpeedingTickets(LicenseNumber, IncidentDate) INCLUDE(TicketAmount);
DECIMAL(7,2)
と聞くかもしれません 、 本当?これらの人々はどれくらい速く進んでいますか?たとえば、カナダでは、10,000ドルのスピード違反の罰金を得るのはそれほど難しいことではありません。
それでは、テーブルにサンプルデータを入力してみましょう。ここではすべての詳細については説明しませんが、これにより、1か月の期間にわたって複数のドライバーと複数のチケット金額を表す約6,000行が生成されるはずです。
;WITH TicketAmounts(ID,Value) AS ( -- 10 arbitrary ticket amounts SELECT i,p FROM ( VALUES(1,32.75),(2,75), (3,109),(4,175),(5,295), (6,68.50),(7,125),(8,145),(9,199),(10,250) ) AS v(i,p) ), LicenseNumbers(LicenseNumber,[newid]) AS ( -- 1000 random license numbers SELECT TOP (1000) 7000000 + number, n = NEWID() FROM [master].dbo.spt_values WHERE number BETWEEN 1 AND 999999 ORDER BY n ), JanuaryDates([day]) AS ( -- every day in January 2014 SELECT TOP (31) DATEADD(DAY, number, '20140101') FROM [master].dbo.spt_values WHERE [type] = N'P' ORDER BY number ), Tickets(LicenseNumber,[day],s) AS ( -- match *some* licenses to days they got tickets SELECT DISTINCT l.LicenseNumber, d.[day], s = RTRIM(l.LicenseNumber) FROM LicenseNumbers AS l CROSS JOIN JanuaryDates AS d WHERE CHECKSUM(NEWID()) % 100 = l.LicenseNumber % 100 AND (RTRIM(l.LicenseNumber) LIKE '%' + RIGHT(CONVERT(CHAR(8), d.[day], 112),1) + '%') OR (RTRIM(l.LicenseNumber+1) LIKE '%' + RIGHT(CONVERT(CHAR(8), d.[day], 112),1) + '%') ) INSERT dbo.SpeedingTickets(LicenseNumber,IncidentDate,TicketAmount) SELECT t.LicenseNumber, t.[day], ta.Value FROM Tickets AS t INNER JOIN TicketAmounts AS ta ON ta.ID = CONVERT(INT,RIGHT(t.s,1))-CONVERT(INT,LEFT(RIGHT(t.s,2),1)) ORDER BY t.[day], t.LicenseNumber;
これは少し複雑すぎるように思えるかもしれませんが、これらのブログ投稿を作成するときに私がよく抱える最大の課題の1つは、適切な量の現実的な「ランダム」/任意のデータを作成することです。任意のデータを取り込むためのより良い方法がある場合は、どうしても、私のつぶやきを例として使用しないでください。これらは、この投稿の要点の周辺にあります。
アプローチ
T-SQLでこの問題を解決するには、さまざまな方法があります。これが7つのアプローチとそれに関連する計画です。カーソル(間違いなく遅くなるため)や日付ベースの再帰CTE(連続する日に依存するため)などの手法は省略しました。
サブクエリ#1
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets AS s WHERE s.LicenseNumber = o.LicenseNumber AND s.IncidentDate < o.IncidentDate ), 0) FROM dbo.SpeedingTickets AS o ORDER BY LicenseNumber, IncidentDate;
サブクエリ#1の計画
サブクエリ#2
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = ( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets WHERE LicenseNumber = t.LicenseNumber AND IncidentDate <= t.IncidentDate ) FROM dbo.SpeedingTickets AS t ORDER BY LicenseNumber, IncidentDate;>
サブクエリ#2の計画
自己参加
SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal = SUM(t2.TicketAmount) FROM dbo.SpeedingTickets AS t1 INNER JOIN dbo.SpeedingTickets AS t2 ON t1.LicenseNumber = t2.LicenseNumber AND t1.IncidentDate >= t2.IncidentDate GROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount ORDER BY t1.LicenseNumber, t1.IncidentDate;
外部適用
SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal = SUM(t2.TicketAmount) FROM dbo.SpeedingTickets AS t1 OUTER APPLY ( SELECT TicketAmount FROM dbo.SpeedingTickets WHERE LicenseNumber = t1.LicenseNumber AND IncidentDate <= t1.IncidentDate ) AS t2 GROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount ORDER BY t1.LicenseNumber, t1.IncidentDate;
RANGEを使用したSUMOVER()(2012以降のみ)
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate RANGE UNBOUNDED PRECEDING ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate;
RANGEを使用してSUMOVER()を計画する
ROWSを使用したSUMOVER()(2012以降のみ)
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate ROWS UNBOUNDED PRECEDING ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate;
ROWSを使用してSUMOVER()を計画する
セットベースの反復
SQL Server MVP Deep Dives Volume#1の第4章のHugo Kornelis(@Hugo_Kornelis)の功績により、このアプローチはセットベースのアプローチとカーソルアプローチを組み合わせたものです。
DECLARE @x TABLE ( LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL, PRIMARY KEY(LicenseNumber, IncidentDate) ); INSERT @x(LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn) SELECT LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTITION BY LicenseNumber ORDER BY IncidentDate) FROM dbo.SpeedingTickets; DECLARE @rn INT = 1, @rc INT = 1; WHILE @rc > 0 BEGIN SET @rn += 1; UPDATE [current] SET RunningTotal = [last].RunningTotal + [current].TicketAmount FROM @x AS [current] INNER JOIN @x AS [last] ON [current].LicenseNumber = [last].LicenseNumber AND [last].rn = @rn - 1 WHERE [current].rn = @rn; SET @rc = @@ROWCOUNT; END SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal FROM @x ORDER BY LicenseNumber, IncidentDate;
その性質上、このアプローチは、テーブル変数を更新するプロセスで多くの同一のプランを生成します。これらはすべて、自己結合および外部適用プランに似ていますが、シークを使用できます。
セットベースの反復によって作成された多くのUPDATEプランの1つ
各反復の各プランの唯一の違いは、行数です。各反復で影響を受ける行の数は、その日数(より正確には、その「ランク」)。
パフォーマンス結果
SQL Sentry Plan Explorerに示されているように、アプローチがどのように積み重なっているかを次に示します。ただし、セットベースの反復アプローチは、多くの個別のステートメントで構成されているため、他のステートメントと比較するとうまく表現されません。
7つのアプローチのうち6つのPlanExplorerランタイムメトリック>
Plan Explorerでプランを確認し、ランタイムメトリックを比較することに加えて、ManagementStudioで生のランタイムも測定しました。各クエリを10回実行した結果は次のとおりです。これには、SSMSでのレンダリング時間も含まれることに注意してください。
7つのアプローチすべて(10回の反復)の実行時間(ミリ秒) )
したがって、SQL Server 2012以降を使用している場合、最善のアプローチはSUM OVER()
のようです。 ROWS UNBOUNDED PRECEDING
を使用する 。 SQL Server 2012を使用していない場合、たとえばOUTER APPLY
と比較して読み取り数が多いにもかかわらず、2番目のサブクエリアプローチは実行時間の点で最適であるように見えました。 クエリ。もちろん、すべての場合において、スキーマに適合したこれらのアプローチを、独自のシステムに対してテストする必要があります。データ、インデックス、およびその他の要因により、環境に最適な別のソリューションがもたらされる可能性があります。
その他の複雑さ
現在、一意のインデックスは、特定のドライバーが特定の日に複数のチケットを取得した場合に、LicenseNumber+IncidentDateの組み合わせに単一の累積合計が含まれることを意味します。このビジネスルールは、ロジックを少し単純化するのに役立ち、決定論的な累計を生成するためのタイブレーカーの必要性を回避します。
特定のLicenseNumber+IncidentDateの組み合わせに対して複数の行がある場合は、組み合わせを一意にするのに役立つ別の列を使用してタイを解除できます(明らかに、ソーステーブルにはこれらの2つの列に対する一意の制約がありません) 。これは、DATE
の場合でも可能であることに注意してください 列は実際にはDATETIME
–多くの人は、日付/時刻の値が一意であると想定していますが、粒度に関係なく、これが常に保証されるとは限りません。
私の場合、IDENTITY
を使用できます 列、IncidentID
;各ソリューションを調整する方法は次のとおりです(より良い方法がある可能性があることを認め、アイデアを捨てるだけです):
/* --------- subquery #1 --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets AS s WHERE s.LicenseNumber = o.LicenseNumber AND (s.IncidentDate < o.IncidentDate -- added this line: OR (s.IncidentDate = o.IncidentDate AND s.IncidentID < o.IncidentID)) ), 0) FROM dbo.SpeedingTickets AS o ORDER BY LicenseNumber, IncidentDate; /* --------- subquery #2 --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = ( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets WHERE LicenseNumber = t.LicenseNumber AND IncidentDate <= t.IncidentDate -- added this line: AND IncidentID <= t.IncidentID ) FROM dbo.SpeedingTickets AS t ORDER BY LicenseNumber, IncidentDate; /* --------- self-join --------- */ SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal = SUM(t2.TicketAmount) FROM dbo.SpeedingTickets AS t1 INNER JOIN dbo.SpeedingTickets AS t2 ON t1.LicenseNumber = t2.LicenseNumber AND t1.IncidentDate >= t2.IncidentDate -- added this line: AND t1.IncidentID >= t2.IncidentID GROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount ORDER BY t1.LicenseNumber, t1.IncidentDate; /* --------- outer apply --------- */ SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal = SUM(t2.TicketAmount) FROM dbo.SpeedingTickets AS t1 OUTER APPLY ( SELECT TicketAmount FROM dbo.SpeedingTickets WHERE LicenseNumber = t1.LicenseNumber AND IncidentDate <= t1.IncidentDate -- added this line: AND IncidentID <= t1.IncidentID ) AS t2 GROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount ORDER BY t1.LicenseNumber, t1.IncidentDate; /* --------- SUM() OVER using RANGE --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID RANGE UNBOUNDED PRECEDING -- added this column ^^^^^^^^^^^^ ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- SUM() OVER using ROWS --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal = SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID ROWS UNBOUNDED PRECEDING -- added this column ^^^^^^^^^^^^ ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- set-based iteration --------- */ DECLARE @x TABLE ( -- added this column, and made it the PK: IncidentID INT PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL ); -- added the additional column to the INSERT/SELECT: INSERT @x(IncidentID, LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn) SELECT IncidentID, LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID) -- and added this tie-breaker column ------------------------------^^^^^^^^^^^^ FROM dbo.SpeedingTickets; -- the rest of the set-based iteration solution remained unchanged
あなたが遭遇するかもしれないもう一つの厄介な問題は、あなたがテーブル全体ではなく、サブセット(この場合、1月の最初の週)を追いかけているときです。 WHERE
を追加して調整する必要があります 節を作成し、サブクエリを相互に関連付ける場合は、これらの述語を念頭に置いてください。