私は同様の問題を見てきましたが、データに対して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_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;