現在の合計を計算しようとしています。ただし、累積合計が別の列の値よりも大きい場合はリセットする必要があります
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
でより良い代替手段はありますか?
私は同様の問題を見てきましたが、データに対して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の制限がある場合は、コードを変更する必要があります。
データの生成に使用したコードの下。 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
に対して実行する作業がなくなると、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;
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
ウィンドウ化されていないが、純粋な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;
問題を攻撃するためのいくつかのクエリ/メソッドがあるようですが、あなたは私たちを提供していません-あるいは考えさえしていませんか? -テーブルのインデックス。
テーブルにはどのインデックスがありますか?それはヒープですか、それともクラスター化インデックスがありますか?
私はこのインデックスを追加した後に提案されたさまざまな解決策を試します:
(grp, id) INCLUDE (val, reset_val)
または、クラスタ化インデックスを(grp, id)
に変更(または作成)します。
特定のクエリを対象とするインデックスを作成すると、効率が向上するはずです。