Saya telah melihat masalah yang sama dan tidak pernah dapat menemukan solusi fungsi jendela yang melakukan satu kali melewati data. Saya pikir itu tidak mungkin. Fungsi jendela harus dapat diterapkan ke semua nilai dalam kolom. Itu membuat perhitungan reset seperti ini sangat sulit, karena satu reset mengubah nilai untuk semua nilai berikut.
Salah satu cara untuk memikirkan masalah ini adalah Anda bisa mendapatkan hasil akhir yang Anda inginkan jika Anda menghitung total running dasar selama Anda bisa mengurangi total running dari baris sebelumnya yang benar. Misalnya, dalam data sampel Anda nilai untuk id
4 adalah running total of row 4 - the running total of row 3
. Nilai untuk id
6 adalah running total of row 6 - the running total of row 3
karena reset belum terjadi. Nilai untuk id
7 adalah running total of row 7 - the running total of row 6
dan seterusnya.
Saya akan mendekati ini dengan T-SQL dalam satu lingkaran. Saya sedikit terbawa suasana dan berpikir saya punya solusi lengkap. Selama 3 juta baris dan 500 grup kode selesai dalam 24 detik di desktop saya. Saya menguji dengan edisi Pengembang SQL Server 2016 dengan 6 vCPU. Saya mengambil keuntungan dari sisipan paralel dan eksekusi paralel secara umum sehingga Anda mungkin perlu mengubah kode jika Anda menggunakan versi yang lebih lama atau memiliki batasan DOP.
Di bawah kode yang saya gunakan untuk menghasilkan data. Rentang pada VAL
dan RESET_VAL
harus serupa dengan data sampel Anda.
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;
Algoritma adalah sebagai berikut:
1) Mulailah dengan memasukkan semua baris dengan total running standar ke tabel temp.
2) Dalam satu lingkaran:
2a) Untuk setiap grup, hitung baris pertama dengan total running di atas reset_value yang tersisa di tabel dan simpan id, total running yang terlalu besar, dan total running sebelumnya yang terlalu besar di tabel temp.
2b) Hapus baris dari tabel temp pertama ke tabel temp hasil yang memiliki ID
kurang dari atau sama dengan ID
di tabel temp kedua. Gunakan kolom lain untuk menyesuaikan total berjalan sesuai kebutuhan.
3) Setelah penghapusan, tidak ada lagi proses, baris menjalankan tambahan DELETE OUTPUT
ke tabel hasil. Ini untuk baris di akhir grup yang tidak pernah melebihi nilai reset.
Saya akan melalui satu implementasi algoritma di atas dalam langkah-demi-langkah T-SQL.
Mulai dengan membuat beberapa tabel temp. #initial_results
memegang data asli dengan total running standar, #group_bookkeeping
diperbarui setiap loop untuk mengetahui baris mana yang dapat dipindahkan, dan #final_results
berisi hasilnya dengan total running yang disesuaikan untuk reset.
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;
Saya membuat indeks berkerumun di tabel temp setelah begitu memasukkan dan membangun indeks dapat dilakukan secara paralel. Membuat perbedaan besar pada mesin saya tetapi mungkin tidak pada mesin Anda. Membuat indeks pada tabel sumber sepertinya tidak membantu tetapi itu bisa membantu pada mesin Anda.
Kode di bawah ini berjalan dalam loop dan memperbarui tabel pembukuan. Untuk setiap grup, kita perlu mendapatkan hasil maksimum ID
yang harus dipindahkan ke tabel hasil. Kita membutuhkan total running dari baris itu sehingga kita bisa mengurangkannya dari total running awal. The grp_done
kolom diatur ke 1 ketika tidak ada lagi pekerjaan yang harus dilakukan untuk 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);
Benar-benar bukan penggemar LOOP JOIN
petunjuk secara umum, tetapi ini adalah permintaan sederhana dan itu adalah cara tercepat untuk mendapatkan apa yang saya inginkan. Untuk benar-benar mengoptimalkan waktu respons, saya ingin bergabung loop paralel bersarang alih-alih bergabung menggabungkan DOP 1.
Kode di bawah ini berjalan dalam loop dan memindahkan data dari tabel awal ke tabel hasil akhir. Perhatikan penyesuaian terhadap total running awal.
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;
Untuk kenyamanan Anda, di bawah ini adalah kode lengkap:
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;