別の列に基づいて積算合計をリセットする


10

現在の合計を計算しようとしています。ただし、累積合計が別の列の値よりも大きい場合はリセットする必要があります

create table #reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
)

insert into #reset_runn_total
values 
(1,10,1),
(8,12,1),(6,14,1),(5,10,1),(6,13,1),(3,11,1),(9,8,1),(10,12,1)


SELECT Row_number()OVER(partition BY grp ORDER BY id)AS rn,*
INTO   #test
FROM   #reset_runn_total

インデックスの詳細:

CREATE UNIQUE CLUSTERED INDEX ix_load_reset_runn_total
  ON #test(rn, grp) 

サンプルデータ

+----+-----+-----------+-----+
| id | val | reset_val | Grp |
+----+-----+-----------+-----+
|  1 |   1 |        10 | 1   |
|  2 |   8 |        12 | 1   |
|  3 |   6 |        14 | 1   |
|  4 |   5 |        10 | 1   |
|  5 |   6 |        13 | 1   |
|  6 |   3 |        11 | 1   |
|  7 |   9 |         8 | 1   |
|  8 |  10 |        12 | 1   |
+----+-----+-----------+-----+ 

期待される結果

+----+-----+-----------------+-------------+
| id | val |    reset_val    | Running_tot |
+----+-----+-----------------+-------------+
|  1 |   1 | 10              |       1     |  
|  2 |   8 | 12              |       9     |  --1+8
|  3 |   6 | 14              |       15    |  --1+8+6 -- greater than reset val
|  4 |   5 | 10              |       5     |  --reset 
|  5 |   6 | 13              |       11    |  --5+6
|  6 |   3 | 11              |       14    |  --5+6+3 -- greater than reset val
|  7 |   9 | 8               |       9     |  --reset -- greater than reset val 
|  8 |  10 | 12              |      10     |  --reset
+----+-----+-----------------+-------------+

クエリ:

を使用して結果を得ましたRecursive CTE。元の質問はこちら/programming/42085404/reset-running-total-based-on-another-column

;WITH cte
     AS (SELECT rn,id,
                val,
                reset_val,
                grp,
                val                   AS running_total,
                Iif (val > reset_val, 1, 0) AS flag
         FROM   #test
         WHERE  rn = 1
         UNION ALL
         SELECT r.*,
                Iif(c.flag = 1, r.val, c.running_total + r.val),
                Iif(Iif(c.flag = 1, r.val, c.running_total + r.val) > r.reset_val, 1, 0)
         FROM   cte c
                JOIN #test r
                  ON r.grp = c.grp
                     AND r.rn = c.rn + 1)
SELECT *
FROM   cte 

T-SQLを使用せずに、より良い代替手段はありCLRますか?


より良い方法は?このクエリはパフォーマンスが低いですか?どの指標を使用していますか?
アーロンバートランド

@AaronBertrand-理解を深めるために、1つのグループのみのサンプルデータを投稿しました。Idの周りの50000グループについても同じことをしなければなりません。したがって、レコードの合計数はになります。確かに上手くスケールしないでしょう。事務所に戻ったときに指標を更新します。この記事で使用したように、sqlperformance.com / 2012/07 / t60 3000000Recursive CTE3000000sum()Over(Order by)
Pரதீப்

カーソルは再帰的なCTEよりも優れている可能性があります
パパラッツォ

回答:


6

私は同様の問題を見てきましたが、データに対して1回のパスを実行するウィンドウ関数ソリューションを見つけることができませんでした。それは可能ではないと思います。ウィンドウ関数は、列のすべての値に適用できる必要があります。1回のリセットで次のすべての値が変更されるため、このようなリセット計算は非常に困難になります。

問題について考える1つの方法は、正しい前の行から現在の合計を差し引くことができる限り、基本的な現在の合計を計算すれば、必要な最終結果を得ることができるということです。たとえば、サンプルデータでは、id4 の値はrunning total of row 4 - the running total of row 3です。id6 の値は、running total of row 6 - the running total of row 3リセットがまだ行われていないためです。id7 の値はrunning total of row 7 - the running total of row 6などです。

ループでT-SQLを使用してこれに取り組みます。私は少し夢中になり、完全な解決策があると思います。300万行と500グループの場合、コードはデスクトップ上で24秒で終了しました。6 vCPUを搭載したSQL Server 2016 Developerエディションでテストしています。私は一般的に並列挿入と並列実行を利用しているため、古いバージョンを使用している場合やDOPの制限がある場合は、コードを変更する必要があります。

データの生成に使用したコードの下。範囲はサンプルデータと同じVALであるRESET_VAL必要があります。

drop table if exists reset_runn_total;

create table reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
);

DECLARE 
@group_num INT,
@row_num INT;
BEGIN
    SET NOCOUNT ON;
    BEGIN TRANSACTION;

    SET @group_num = 1;
    WHILE @group_num <= 50000 
    BEGIN
        SET @row_num = 1;
        WHILE @row_num <= 60
        BEGIN
            INSERT INTO reset_runn_total WITH (TABLOCK)
            SELECT 1 + ABS(CHECKSUM(NewId())) % 10, 8 + ABS(CHECKSUM(NewId())) % 8, @group_num;

            SET @row_num = @row_num + 1;
        END;
        SET @group_num = @group_num + 1;
    END;
    COMMIT TRANSACTION;
END;

アルゴリズムは次のとおりです。

1)標準の積算合計を含むすべての行を一時テーブルに挿入することから始めます。

2)ループ内:

2a)各グループについて、テーブルに残っているreset_valueを超える実行中の合計を含む最初の行を計算し、ID、大きすぎる実行中の合計、および一時テーブルで大きすぎる前の実行中の合計を格納します。

2b)最初の一時テーブルから、2番目の一時テーブルIDと同じかそれ以下の結果一時テーブルに行を削除しIDます。他の列を使用して、必要に応じて積算合計を調整します。

3)削除が処理されなくなった後、行を追加DELETE OUTPUTして結果テーブルに追加します。これは、リセット値を決して超えないグループの最後の行用です。

上記のアルゴリズムの1つの実装を、T-SQLで段階的に説明します。

いくつかの一時テーブルを作成することから始めます。#initial_resultsは、標準の積算合計で元のデータを保持し、#group_bookkeeping移動できる行を特定するためにループごとに更新#final_resultsされ、リセットのために調整された積算合計の結果を含みます。

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

一時テーブルにクラスター化インデックスを作成した後、挿入とインデックス作成を並行して実行できるようにします。私のマシンでは大きな違いがありましたが、あなたのマシンではそうではないかもしれません。ソーステーブルにインデックスを作成することは役に立たなかったようですが、それはあなたのマシンで役立つかもしれません。

以下のコードはループで実行され、簿記テーブルを更新します。グループごとIDに、結果テーブルに移動する必要がある最大値を見つける必要があります。その行の積算合計が必要なので、最初の積算合計から減算できます。grp_done以下のために行うには、それ以上の仕事がないときに列が1に設定されていますgrp

WITH UPD_CTE AS (
        SELECT 
        #grp_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_update
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        INNER JOIN #initial_results IR ON #group_bookkeeping.grp = ir.grp
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_update
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
OPTION (LOOP JOIN);

本当にLOOP JOIN一般的にヒントのファンではありませんが、これは単純なクエリであり、私が欲しかったものを取得する最も速い方法でした。応答時間を本当に最適化するために、DOP 1マージ結合ではなく並列ネストループ結合が必要でした。

以下のコードはループで実行され、データを初期テーブルから最終結果テーブルに移動します。最初の累計に対する調整に注意してください。

DELETE ir
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
WHERE tb.grp_done = 0;

以下にあなたの便宜のために完全なコードを示します:

DECLARE @RC INT;
BEGIN
SET NOCOUNT ON;

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

SET @RC = 1;
WHILE @RC > 0 
BEGIN
    WITH UPD_CTE AS (
        SELECT 
        #group_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_move
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        CROSS APPLY (SELECT ID, RESET_VAL, initial_running_total FROM #initial_results ir WHERE #group_bookkeeping.grp = ir.grp ) ir
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_move
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
    OPTION (LOOP JOIN);

    DELETE ir
    OUTPUT DELETED.id,  
        DELETED.VAL,  
        DELETED.RESET_VAL,  
        DELETED.GRP ,
        DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
    FROM #initial_results ir
    INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
    WHERE tb.grp_done = 0;

    SET @RC = @@ROWCOUNT;
END;

DELETE ir 
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP;

CREATE CLUSTERED INDEX f1 ON #final_results (grp, id);

/* -- do something with the data
SELECT *
FROM #final_results
ORDER BY grp, id;
*/

DROP TABLE #final_results;
DROP TABLE #initial_results;
DROP TABLE #group_bookkeeping;

END;

単に素晴らしい私はあなたに賞金を授与します
Pரதீப்

私たちのサーバーでは、50000 grpと60 idの場合、1分10秒かかりました。Recursive CTE2分15秒かかりました
2017

両方のコードを同じデータでテストしました。あなたのは素晴らしかった。さらに改善できますか?
Pரதீப்2017

つまり、実際のデータでコードを実行し、テストしました。計算は実際の手順で一時テーブルで処理されますが、おそらく密にパックされているはずです。30秒前後の任意の時間に削減できるとよいでしょう
Pரதீப்

@Prdp更新を使用する迅速なアプローチを試しましたが、それはさらに悪いようでした。しばらくこれを詳しく調べることができません。各操作にかかる時間をログに記録して、サーバーでどの部分が最も遅く実行されているかを把握できるようにします。このコードまたはより一般的なアルゴリズムを高速化する方法があることは間違いありません。
ジョー・オブビッシュ

4

CURSORの使用:

ALTER TABLE #reset_runn_total ADD RunningTotal int;

DECLARE @id int, @val int, @reset int, @acm int, @grp int, @last_grp int;
SET @acm = 0;

DECLARE curRes CURSOR FAST_FORWARD FOR 
SELECT id, val, reset_val, grp
FROM #reset_runn_total
ORDER BY grp, id;

OPEN curRes;
FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
SET @last_grp = @grp;

WHILE @@FETCH_STATUS = 0  
BEGIN
    IF @grp <> @last_grp SET @acm = 0;
    SET @last_grp = @grp;
    SET @acm = @acm + @val;
    UPDATE #reset_runn_total
    SET RunningTotal = @acm
    WHERE id = @id;
    IF @acm > @reset SET @acm = 0;
    FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
END

CLOSE curRes;
DEALLOCATE curRes;

+----+-----+-----------+-------------+
| id | val | reset_val | RunningTotal|
+----+-----+-----------+-------------+
| 1  | 1   | 10        |     1       |
+----+-----+-----------+-------------+
| 2  | 8   | 12        |     9       |
+----+-----+-----------+-------------+
| 3  | 6   | 14        |     15      |
+----+-----+-----------+-------------+
| 4  | 5   | 10        |     5       |
+----+-----+-----------+-------------+
| 5  | 6   | 13        |     11      |
+----+-----+-----------+-------------+
| 6  | 3   | 11        |     14      |
+----+-----+-----------+-------------+
| 7  | 9   | 8         |     9       |
+----+-----+-----------+-------------+
| 8  | 10  | 12        |     10      |
+----+-----+-----------+-------------+

ここをチェックしてください:http//rextester.com/WSPLO95303


3

ウィンドウ化されていませんが、純粋なSQLバージョン:

WITH x AS (
    SELECT TOP 1 id,
           val,
           reset_val,
           val AS running_total,
           1 AS level 
      FROM reset_runn_total
    UNION ALL
    SELECT r.id,
           r.val,
           r.reset_val,
           CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END,
           level = level + 1
      FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
) SELECT
  *
FROM x
WHERE NOT EXISTS (
        SELECT 1
        FROM x AS x2
        WHERE x2.id = x.id
        AND x2.level > x.level
    )
ORDER BY id, level DESC
;

私はSQL Serverの方言の専門家ではありません。これはPostrgreSQLの初期バージョンです(私が正しく理解していると、SQL Serverの再帰部分でLIMIT 1 / TOP 1を使用できません)。

WITH RECURSIVE x AS (
    (SELECT id, val, reset_val, val AS running_total
       FROM reset_runn_total
      ORDER BY id
      LIMIT 1)
    UNION
    (SELECT r.id, r.val, r.reset_val,
            CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END
       FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
      ORDER BY id
      LIMIT 1)
) SELECT * FROM x;

@JoeObbishは正直に言って、それは質問から完全に明確ではありません。たとえば、期待される結果にはgrp列が表示されません。
ypercubeᵀᴹ

@JoeObbishも私が理解したことです。それでも、質問はそれについての明確な声明から利益を得ることができます。質問のコード(CTEあり)もそれを使用していません(名前が異なる列も含まれています)。質問を読む人にとっては、他の答えやコメントを読む必要はないでしょう-すべきではない-はずです。
ypercubeᵀᴹ

@ypercubeᵀᴹ質問に必要な情報を追加しました。
Pரதீப்2017

1

問題を攻撃するためのいくつかのクエリ/メソッドがあるようですが、あなたは私たちを提供していません-あるいは考えさえしていませんか?-テーブルのインデックス。

テーブルにはどのインデックスがありますか?それはヒープですか、それともクラスター化インデックスがありますか?

私はこのインデックスを追加した後に提案されたさまざまな解決策を試します:

(grp, id) INCLUDE (val, reset_val)

または、クラスタ化インデックスをに変更(または作成)します(grp, id)

特定のクエリを対象とするインデックスを作成すると、効率が向上するはずです。


質問に必要な情報を追加しました。
Pரதீப்2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.