SQL Serverで実行中の合計を計算する


170

次の表(と呼ばれるTestTable)を想像してみてください。

id     somedate    somevalue
--     --------    ---------
45     01/Jan/09   3
23     08/Jan/09   5
12     02/Feb/09   0
77     14/Feb/09   7
39     20/Feb/09   34
33     02/Mar/09   6

次のように、日付順に実行合計を返すクエリが必要です。

id     somedate    somevalue  runningtotal
--     --------    ---------  ------------
45     01/Jan/09   3          3
23     08/Jan/09   5          8
12     02/Feb/09   0          8
77     14/Feb/09   7          15  
39     20/Feb/09   34         49
33     02/Mar/09   6          55

SQL Server 2000/2005/2008でこれを行うにはさまざまな方法があります。

aggregating-set-statementトリックを使用するこの種のメソッドに特に興味があります。

INSERT INTO @AnotherTbl(id, somedate, somevalue, runningtotal) 
   SELECT id, somedate, somevalue, null
   FROM TestTable
   ORDER BY somedate

DECLARE @RunningTotal int
SET @RunningTotal = 0

UPDATE @AnotherTbl
SET @RunningTotal = runningtotal = @RunningTotal + somevalue
FROM @AnotherTbl

...これは非常に効率的ですが、UPDATEステートメントが行を正しい順序で処理することを必ずしも保証できないため、これに関する問題があると聞きました。多分私達はその問題についてのいくつかの決定的な答えを得ることができます。

しかし、人々が提案できる他の方法があるのでしょうか?

編集:今、上記の設定と「更新トリック」の例を含むSqlFiddle


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx 注文を更新に追加...セットすると、保証が得られます。
Simon D

しかし、ORDER BYをUPDATEステートメントに適用することはできません...
codeulike '12

参照してくださいsqlperformance.com/2012/07/t-sql-queries/running-totalsは、SQL Server 2012を使用している場合は特に
アーロン・ベルトラン

回答:


133

更新、あなたはSQL Serverを実行している場合、2012を参照してください:https://stackoverflow.com/a/10309947

問題は、SQL ServerのOver句の実装がいくらか制限されていることです。

Oracle(およびANSI-SQL)では、次のようなことができます。

 SELECT somedate, somevalue,
  SUM(somevalue) OVER(ORDER BY somedate 
     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 
          AS RunningTotal
  FROM Table

SQL Serverでは、この問題に対する明確な解決策はありません。私の直感は、これがカーソルが最も速いまれなケースの1つであると私に伝えていますが、大きな結果についてはいくつかのベンチマークを行う必要があります。

アップデートのトリックは便利ですが、私はかなり壊れやすいと感じています。テーブル全体を更新する場合、主キーの順に処理されるようです。したがって、日付を昇順の主キーとして設定するとprobably、安全です。しかし、ドキュメントに記載されていないSQL Server実装の詳細に依存しています(クエリが2つのprocによって実行されてしまう場合、何が起こるのだろうか、MAXDOPを参照してください)。

完全に機能するサンプル:

drop table #t 
create table #t ( ord int primary key, total int, running_total int)

insert #t(ord,total)  values (2,20)
-- notice the malicious re-ordering 
insert #t(ord,total) values (1,10)
insert #t(ord,total)  values (3,10)
insert #t(ord,total)  values (4,1)

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t
order by ord 

ord         total       running_total
----------- ----------- -------------
1           10          10
2           20          30
3           10          40
4           1           41

あなたはこれがローダウンであるベンチマークを求めました。

これを行う最も高速なSAFE方法はカーソルです。これは、クロス結合の相関サブクエリよりも桁違いに高速です。

最速の方法は、UPDATEトリックです。私の唯一の懸念は、すべての状況で更新が直線的に進行するかどうか確信がないことです。明示的にそう言っているクエリには何もありません。

結論として、量産コードではカーソルを使用します。

テストデータ:

create table #t ( ord int primary key, total int, running_total int)

set nocount on 
declare @i int
set @i = 0 
begin tran
while @i < 10000
begin
   insert #t (ord, total) values (@i,  rand() * 100) 
    set @i = @i +1
end
commit

テスト1:

SELECT ord,total, 
    (SELECT SUM(total) 
        FROM #t b 
        WHERE b.ord <= a.ord) AS b 
FROM #t a

-- CPU 11731, Reads 154934, Duration 11135 

テスト2:

SELECT a.ord, a.total, SUM(b.total) AS RunningTotal 
FROM #t a CROSS JOIN #t b 
WHERE (b.ord <= a.ord) 
GROUP BY a.ord,a.total 
ORDER BY a.ord

-- CPU 16053, Reads 154935, Duration 4647

テスト3:

DECLARE @TotalTable table(ord int primary key, total int, running_total int)

DECLARE forward_cursor CURSOR FAST_FORWARD 
FOR 
SELECT ord, total
FROM #t 
ORDER BY ord


OPEN forward_cursor 

DECLARE @running_total int, 
    @ord int, 
    @total int
SET @running_total = 0

FETCH NEXT FROM forward_cursor INTO @ord, @total 
WHILE (@@FETCH_STATUS = 0)
BEGIN
     SET @running_total = @running_total + @total
     INSERT @TotalTable VALUES(@ord, @total, @running_total)
     FETCH NEXT FROM forward_cursor INTO @ord, @total 
END

CLOSE forward_cursor
DEALLOCATE forward_cursor

SELECT * FROM @TotalTable

-- CPU 359, Reads 30392, Duration 496

テスト4:

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t

-- CPU 0, Reads 58, Duration 139

1
ありがとう。したがって、コードサンプルは、主キーの順序で合計されることを示すことになると思います。カーソルが、より大きなデータセットの結合よりもさらに効率的かどうかを知ることは興味深いでしょう。
codeulike、2009年

1
私はCTE @Martinをテストしましたが、更新のトリックに近づくものは何もありません-カーソルは読み取りで低く見えます。これがプロファイラトレースですi.stack.imgur.com/BbZq3.png
Sam Saffron


1
この回答に加えられたすべての作業に+1-更新オプションが大好きです。パーティションをこのUPDATEスクリプトに組み込むことができますか?たとえば、「Car Colour」という追加のフィールドがある場合、このスクリプトは各「Car Colour」パーティション内の現在の合計を返すことができますか?
whytheq

2
最初の(Oracle(およびANSI-SQL))回答がSQL Server 2017で機能するようになりました。
DaniDev


40

Sam Saffronはそれについて素晴らしい仕事をしましたが、この問題のために再帰的な共通テーブル式コードをまだ提供していませんでした。デナリではなくSQL Server 2008 R2を使用している私たちにとっては、合計を実行するのに依然として最速の方法であり、作業用コンピューターのカーソルよりも10万行高速であり、インラインクエリでもあります。
したがって、ここにあります(ordテーブルに列があり、ギャップのない連続番号であると想定しています。高速処理のために、この番号にも一意の制約があるはずです):

;with 
CTE_RunningTotal
as
(
    select T.ord, T.total, T.total as running_total
    from #t as T
    where T.ord = 0
    union all
    select T.ord, T.total, T.total + C.running_total as running_total
    from CTE_RunningTotal as C
        inner join #t as T on T.ord = C.ord + 1
)
select C.ord, C.total, C.running_total
from CTE_RunningTotal as C
option (maxrecursion 0)

-- CPU 140, Reads 110014, Duration 132

sql fiddle demo

更新可変または風変わりな更新を使用した この更新についても知りました。通常は問題なく動作しますが、毎回動作することをどのように確認できますか?さて、ここに小さなトリックがあります(ここで見つかります-http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258)-現在と以前ordを確認し1/0、それらが何と異なる場合は割り当てを使用しますあなたは期待しています:

declare @total int, @ord int

select @total = 0, @ord = -1

update #t set
    @total = @total + total,
    @ord = case when ord <> @ord + 1 then 1/0 else ord end,
    ------------------------
    running_total = @total

select * from #t

-- CPU 0, Reads 58, Duration 139

テーブルに適切なクラスター化インデックス/主キー(この場合はによるインデックスord_id)があるかどうかを確認したところ、更新は常に線形に進行します(ゼロによる除算は発生しません)。とは言っても、量産コードで使用するかどうかはあなた次第です:)

アップデート2は、 -それは風変わりなアップデートの信頼性の欠如についてのいくつかの有用な情報が含ま原因私は、この答えをリンクしていnvarchar型連結/インデックス/データ型はnvarchar(max)の不可解な行動を


6
この回答はより多くの認識に値します(または、おそらく私には見られないいくつかの欠陥があるのでしょうか)
user1068352

ord = ord + 1で結合できるように連番が必要ですが、もう少し作業が必要な場合があります。とにかく、SQL 2008 R2ではこのソリューションを使用しています
Roman Pekar

+1 SQLServer2008R2では、再帰的なCTEを使用するアプローチも好みます。参考までに、ギャップを許容するテーブルの値を見つけるために、相関サブクエリを使用します。クエリsqlfiddle.com/#!3/d41d8/18967に
Aleksandr Fedorenko 2013

2
データの序数が既にあり、SQL 2008 R2で簡潔な(カーソル以外の)セットベースのソリューションを探している場合、これは完璧なようです。
Nick.McDermaid 2014

1
実行中のすべてのクエリに、連続する序数フィールドがあるとは限りません。時々、日時フィールドがあなたのものである、またはレコードがソートの途中から削除されたことがあります。それはそれがより頻繁に使用されない理由かもしれません。
2014年

28

SQL 2005以降のAPPLY演算子はこのために機能します。

select
    t.id ,
    t.somedate ,
    t.somevalue ,
    rt.runningTotal
from TestTable t
 cross apply (select sum(somevalue) as runningTotal
                from TestTable
                where somedate <= t.somedate
            ) as rt
order by t.somedate

5
小さなデータセットで非常にうまく機能します。欠点は、内部クエリと外部クエリで同じwhere句を使用する必要があることです。
種牡馬

私の日付の一部はまったく同じだったので(秒の端数まで)、row_number()over(txndateによる順序)を内部テーブルと外部テーブルに追加し、それを実行するためにいくつかの複合インデックスを追加しました。滑らかでシンプルなソリューション。ところで、テストされたクロスクエリはサブクエリに対して適用されます...それはわずかに高速です。
pghcpa 2015年

これは非常にクリーンであり、小さなデータセットでもうまく機能します。再帰CTEよりも高速
jtate

これは(小さなデータセットの場合も)優れたソリューションですが、somedate列が一意であることを意味することにも注意する必要があります
Roman Pekar

11
SELECT TOP 25   amount, 
    (SELECT SUM(amount) 
    FROM time_detail b 
    WHERE b.time_detail_id <= a.time_detail_id) AS Total FROM time_detail a

ROW_NUMBER()関数と一時テーブルを使用して、内部SELECTステートメントの比較で使用する任意の列を作成することもできます。


1
これは本当に非効率的です...しかし、SQLサーバーでこれを行う実際のクリーンな方法はありません
Sam Saffron

絶対に効率的ではありません-しかし、それは機能し、何かが正しい順序で実行されるか、間違った順序で実行されるかについては疑問の余地はありません。
サムアックス

efficienty批判を持っているおかげで、その代替の答えがあると便利、とにも便利
codeulike

7

相関サブクエリを使用します。とても簡単です、ここに行きます:

SELECT 
somedate, 
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
GROUP BY somedate
ORDER BY somedate

コードは正確ではないかもしれませんが、アイデアは確かです。

GROUP BYは、日付が複数回表示される場合に備えて、結果セットで1回だけ表示する必要があります。

繰り返しの日付が表示されることを気にしない場合、または元の値とIDを表示する場合は、次のようになります。

SELECT 
id,
somedate, 
somevalue,
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
ORDER BY somedate

ありがとう...シンプルで良かった。パフォーマンスを向上させるために追加するインデックスがありましたが、それは非常に単純で(データベースエンジンチューニングアドバイザーの推奨事項の1つを採用しました;)、それからショットのように実行されました。
Doug_Ivison 2015


4

SQL Server 2008でウィンドウ処理が他の場所と同じように機能すると(私が試した)、これを試してみます。

select testtable.*, sum(somevalue) over(order by somedate)
from testtable
order by somedate;

MSDNでは、SQL Server 2008(およびおそらく2005年も同様)で利用できると述べていますが、試すためのインスタンスはありません。

編集:まあ、どうやらSQL Serverでは、「PARTITION BY」を指定せずにウィンドウ指定(「OVER(...)」)を許可していません(結果をグループに分割していますが、GROUP BYのように集計していません)。煩わしい-MSDN構文リファレンスは、オプションであることを示唆していますが、現時点ではSqlServer 2000インスタンスしかありません。

私が出したクエリは、Oracle 10.2.0.3.0とPostgreSQL 8.4-betaの両方で機能します。だから、MSに追いつくように伝えなさい;)


2
この場合、SUMでOVERを使用しても、現在の合計が得られません。SUMと共に使用する場合、OVER句はORDER BYを受け入れません。現在の合計では機能しないPARTITION BYを使用する必要があります。
サムアックス

おかげで、これが機能しない理由を聞くのは実際に役立ちます。araqnid多分あなたはそれがオプションではない理由を説明するためにあなたの答えを編集することができます
codeulike


これは実際に機能します。パーティションを作成する必要があるためです。そのため、これは最も一般的な回答ではありませんが、SQLでのRTの問題に対する最も簡単な解決策です。
ウィリアムMB

私はMSSQL 2008を持っていませんが、(select null)でパーティションを作成し、パーティションの問題をハッキングできると思います。または1 partitionme、それによって副選択を作成し、それによって分割します。また、レポートを作成するときの実際の状況では、おそらくパーティション分割が必要です。
ヌレッティン2017

4

上記のSQL Server 2008 R2を使用している場合。次に、それが最も簡単な方法です。

Select id
    ,somedate
    ,somevalue,
LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal
From TestTable 

LAGは、前の行の値を取得するために使用されます。あなたは詳細についてグーグルを行うことができます。

[1]:


1
LAGはSQL Server 2012以降(2008年ではない)にのみ存在すると思います
AaA

1
LAG()を使用しても改善されません。SUM(somevalue) OVER(...) これは私にはかなりきれいに
思え

2

以下の単純なINNER JOIN操作を使用して、現在までの合計を達成できると思います。

SELECT
     ROW_NUMBER() OVER (ORDER BY SomeDate) AS OrderID
    ,rt.*
INTO
    #tmp
FROM
    (
        SELECT 45 AS ID, CAST('01-01-2009' AS DATETIME) AS SomeDate, 3 AS SomeValue
        UNION ALL
        SELECT 23, CAST('01-08-2009' AS DATETIME), 5
        UNION ALL
        SELECT 12, CAST('02-02-2009' AS DATETIME), 0
        UNION ALL
        SELECT 77, CAST('02-14-2009' AS DATETIME), 7
        UNION ALL
        SELECT 39, CAST('02-20-2009' AS DATETIME), 34
        UNION ALL
        SELECT 33, CAST('03-02-2009' AS DATETIME), 6
    ) rt

SELECT
     t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
    ,SUM(t2.SomeValue) AS RunningTotal
FROM
    #tmp t1
    JOIN #tmp t2
        ON t2.OrderID <= t1.OrderID
GROUP BY
     t1.OrderID
    ,t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
ORDER BY
    t1.OrderID

DROP TABLE #tmp

はい、これはサムサフランの回答の「テスト3」に相当すると思います。
codeulike、2011

2

以下は必要な結果を生成します。

SELECT a.SomeDate,
       a.SomeValue,
       SUM(b.SomeValue) AS RunningTotal
FROM TestTable a
CROSS JOIN TestTable b
WHERE (b.SomeDate <= a.SomeDate) 
GROUP BY a.SomeDate,a.SomeValue
ORDER BY a.SomeDate,a.SomeValue

SomeDateにクラスター化インデックスを作成すると、パフォーマンスが大幅に向上します。


@Dave私はこの質問がこれを行う効率的な方法を見つけようとしていると思います。大規模なセットの場合、クロス結合は非常に遅くなります
Sam Saffron

efficienty批判を持っているおかげで、その代替の答えがあると便利、とにも便利
codeulike


2

最善の方法はウィンドウ関数を使用することですが、単純な相関サブクエリを使用して実行することもできます

Select id, someday, somevalue, (select sum(somevalue) 
                                from testtable as t2
                                where t2.id = t1.id
                                and t2.someday <= t1.someday) as runningtotal
from testtable as t1
order by id,someday;

0
BEGIN TRAN
CREATE TABLE #Table (_Id INT IDENTITY(1,1) ,id INT ,    somedate VARCHAR(100) , somevalue INT)


INSERT INTO #Table ( id  ,    somedate  , somevalue  )
SELECT 45 , '01/Jan/09', 3 UNION ALL
SELECT 23 , '08/Jan/09', 5 UNION ALL
SELECT 12 , '02/Feb/09', 0 UNION ALL
SELECT 77 , '14/Feb/09', 7 UNION ALL
SELECT 39 , '20/Feb/09', 34 UNION ALL
SELECT 33 , '02/Mar/09', 6 

;WITH CTE ( _Id, id  ,  _somedate  , _somevalue ,_totvalue ) AS
(

 SELECT _Id , id  ,    somedate  , somevalue ,somevalue
 FROM #Table WHERE _id = 1
 UNION ALL
 SELECT #Table._Id , #Table.id  , somedate  , somevalue , somevalue + _totvalue
 FROM #Table,CTE 
 WHERE #Table._id > 1 AND CTE._Id = ( #Table._id-1 )
)

SELECT * FROM CTE

ROLLBACK TRAN

ここで何をしているのかについていくつかの情報を提供し、この特定の方法の長所/短所に注意する必要があります。
TT。

0

積算合計を計算する2つの簡単な方法を次に示します。

アプローチ1:DBMSが分析機能をサポートしている場合は、このように書くことができます

SELECT     id
           ,somedate
           ,somevalue
           ,runningtotal = SUM(somevalue) OVER (ORDER BY somedate ASC)
FROM       TestTable

アプローチ2:データベースバージョン/ DBMS自体が分析関数をサポートしていない場合は、OUTER APPLYを利用できます。

SELECT     T.id
           ,T.somedate
           ,T.somevalue
           ,runningtotal = OA.runningtotal
FROM       TestTable T
           OUTER APPLY (
                           SELECT   runningtotal = SUM(TI.somevalue)
                           FROM     TestTable TI
                           WHERE    TI.somedate <= S.somedate
                       ) OA;

注:-異なるパーティションの積算合計を個別に計算する必要がある場合は、ここに掲載されているように実行できます:行全体の積算合計の計算とIDによるグループ化

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.