web-dev-qa-db-ja.com

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

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

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を使用して結果を取得しました。元の質問はこちら https://stackoverflow.com/questions/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 

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

10

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

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

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

データの生成に使用したコードの下。 VALRESET_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に対して実行する作業がなくなると、grp_done列は1に設定されます。

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;
6
Joe Obbish

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/WSPLO953

4
McNets

ウィンドウ化されていないが、純粋な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;
3
Roman Tkachuk

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

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

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

(grp, id) INCLUDE (val, reset_val)

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

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

1
ypercubeᵀᴹ