Reset Menjalankan Total berdasarkan kolom lain


10

Saya mencoba untuk menghitung total berjalan. Tetapi harus me-reset ketika jumlah kumulatif lebih besar dari nilai kolom lain

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

Detail indeks:

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

contoh data

+----+-----+-----------+-----+
| 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   |
+----+-----+-----------+-----+ 

Hasil yang diharapkan

+----+-----+-----------------+-------------+
| 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
+----+-----+-----------------+-------------+

Pertanyaan:

Saya mendapat hasilnya menggunakan Recursive CTE. Pertanyaan asli ada di sini /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 

Apakah ada alternatif yang lebih baik T-SQLtanpa menggunakan CLR.?


Lebih baik bagaimana? Apakah kueri ini menunjukkan kinerja yang buruk? Menggunakan metrik apa?
Aaron Bertrand

@AaronBertrand - Untuk pemahaman yang lebih baik, saya telah mengirim data sampel hanya untuk satu grup. Saya harus melakukan hal yang sama untuk sekitar 50000kelompok dengan 60 Id . jadi jumlah total rekaman akan ada 3000000. Yakin Recursive CTEtidak akan skala pada 3000000. Akan memperbarui metrik ketika saya kembali ke kantor. Bisakah kita mencapai ini menggunakan sum()Over(Order by)seperti yang Anda gunakan dalam artikel ini sqlperformance.com/2012/07/t-sql-queries/running-totals
P ரதீப்

Kursor mungkin lebih baik daripada CTE rekursif
paparazzo

Jawaban:


6

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 id4 adalah running total of row 4 - the running total of row 3. Nilai untuk id6 adalah running total of row 6 - the running total of row 3karena reset belum terjadi. Nilai untuk id7 adalah running total of row 7 - the running total of row 6dan 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 VALdan RESET_VALharus 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 IDkurang dari atau sama dengan IDdi tabel temp kedua. Gunakan kolom lain untuk menyesuaikan total berjalan sesuai kebutuhan.

3) Setelah penghapusan, tidak ada lagi proses, baris menjalankan tambahan DELETE OUTPUTke 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_resultsmemegang data asli dengan total running standar, #group_bookkeepingdiperbarui setiap loop untuk mengetahui baris mana yang dapat dipindahkan, dan #final_resultsberisi 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 IDyang harus dipindahkan ke tabel hasil. Kita membutuhkan total running dari baris itu sehingga kita bisa mengurangkannya dari total running awal. The grp_donekolom 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 JOINpetunjuk 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;

cukup luar biasa Aku akan menghadiahkanmu dengan hadiah
P ரதீப்

Di server kami, untuk 50000 grp dan 60 id milik Anda, perlu waktu 1 menit 10 detik. Recursive CTEbutuh 2 menit dan 15 detik
P ரதீப்

Saya menguji kedua kode dengan data yang sama. Anda mengagumkan. Bisakah ini diperbaiki lebih lanjut?
P ரதீப்

Maksud saya, saya menjalankan kode Anda pada data asli kami dan mengujinya. Perhitungan diproses dalam tabel temp di prosedur nyata saya, kemungkinan besar harus dikemas dengan ketat. Akan bagus jika dapat dikurangi menjadi sekitar 30 detik
P ரதீப்

@Prdp Mencoba pendekatan cepat yang menggunakan pembaruan tetapi tampaknya lebih buruk. Tidak akan bisa melihat ini lebih lama. Coba masuk berapa lama setiap operasi sehingga Anda dapat mengetahui bagian mana yang paling lambat berjalan di server Anda. Sangat mungkin bahwa ada cara untuk mempercepat kode ini atau algoritma yang lebih baik secara umum.
Joe Obbish

4

Menggunakan 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      |
+----+-----+-----------+-------------+

Periksa di sini: http://rextester.com/WSPLO95303


3

Tidak berjendela, tetapi versi SQL murni:

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
;

Saya bukan spesialis dalam dialek SQL Server. Ini adalah versi awal untuk PostrgreSQL (jika saya mengerti benar saya tidak bisa menggunakan LIMIT 1 / TOP 1 di bagian rekursif dalam SQL Server):

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;

@ JoObbish jujur, itu tidak sepenuhnya jelas dari pertanyaan. Hasil yang diharapkan misalnya, tidak menunjukkan grpkolom.
ypercubeᵀᴹ

@ JoObbish, itulah yang saya mengerti juga. namun, pertanyaannya bisa mendapatkan manfaat dari pernyataan eksplisit tentang hal itu. Kode dalam pertanyaan (dengan CTE) tidak menggunakannya (dan bahkan memiliki kolom yang berbeda namanya). Jelas bagi siapa saja yang membaca pertanyaan - mereka tidak - dan tidak seharusnya - harus membaca jawaban atau komentar lainnya.
ypercubeᵀᴹ

@ ypercubeᵀᴹ Menambahkan informasi yang diperlukan pada pertanyaan.
P ரதீப்

1

Tampaknya Anda memiliki beberapa pertanyaan / metode untuk menyerang masalah tetapi Anda belum memberikan kami - atau bahkan mempertimbangkan? - indeks di atas meja.

Indeks apa yang ada di tabel? Apakah itu tumpukan atau apakah itu memiliki indeks berkerumun?

Saya akan mencoba berbagai solusi yang disarankan setelah menambahkan indeks ini:

(grp, id) INCLUDE (val, reset_val)

Atau cukup ubah (atau buat) indeks yang dikelompokkan menjadi (grp, id).

Memiliki indeks yang menargetkan permintaan spesifik harus meningkatkan efisiensi - sebagian besar atau semua metode.


Menambahkan informasi yang diperlukan pada pertanyaan.
P ரதீப்
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.