Başka bir sütuna göre Koşu Toplamını Sıfırla


10

Toplam çalışan hesaplamaya çalışıyorum. Ancak kümülatif toplam başka bir sütun değerinden büyük olduğunda sıfırlanmalıdır

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

Endeks detayları:

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

örnek veri

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

Beklenen Sonuç

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

Sorgu:

Sonucu kullanarak aldım Recursive CTE. Orijinal soru burada /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 

T-SQLKullanmadan daha iyi bir alternatif var mı CLR?


Nasıl daha iyi? Bu sorgu düşük performans gösteriyor mu? Hangi metrikleri kullanıyorsunuz?
Aaron Bertrand

@AaronBertrand - Daha iyi anlamak için sadece bir grup için örnek veriler yayınladım. Aynı şeyi kimlikli50000 gruplar için de yapmak zorundayım . yani toplam kayıt sayısı ortada olacak . Eminim iyi ölçeklenmeyecek . Ofise döndüğümde metrikleri güncelleyeceğim. Bunu , bu makalede kullandığınız gibi kullanarak yapabilir miyiz sqlperformance.com/2012/07/t-sql-queries/running-totals60 3000000Recursive CTE3000000sum()Over(Order by)
P ரதீப்

Bir imleç özyinelemeli bir CTE'den daha iyisini yapabilir
paparazzo

Yanıtlar:


6

Ben benzer sorunları baktım ve veri üzerinde tek bir geçiş yapar bir pencere fonksiyonu çözümü bulmak mümkün olmadım. Bunun mümkün olduğunu düşünmüyorum. Pencere işlevlerinin bir sütundaki tüm değerlere uygulanabilmesi gerekir. Bunun gibi sıfırlama hesaplamaları çok zorlaşır, çünkü bir sıfırlama aşağıdaki değerlerin tümünün değerini değiştirir.

Sorunu düşünmenin bir yolu, doğru toplam satırını doğru önceki satırdan çıkarabildiğiniz sürece temel bir koşu toplamını hesaplarsanız istediğiniz sonucu elde edebilmenizdir. Örneğin, örnek verilerinizde id4 değeri running total of row 4 - the running total of row 3. Değeri id6 olan running total of row 6 - the running total of row 3bir sıfırlama henüz olmadı çünkü. id7 değeri running total of row 7 - the running total of row 6vb.

Bu bir döngüde T-SQL ile yaklaşacaktı. Biraz taşındım ve tam bir çözüm bulduğumu düşünüyorum. 3 milyon satır ve 500 grup için kod masaüstümde 24 saniye içinde tamamlandı. 6 vCPU ile SQL Server 2016 Developer sürümü ile test ediyorum. Paralel ekler ve genel olarak paralel yürütme avantajlarından yararlanıyorum, bu nedenle eski bir sürümdeyseniz veya DOP sınırlamalarınız varsa kodu değiştirmeniz gerekebilir.

Verileri oluşturmak için kullandığım kodun altında. Aralıklar VALve RESET_VALörnek verilerinize benzer olmalıdır.

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 aşağıdaki gibidir:

1) Standart bir toplamı olan tüm satırları geçici bir tabloya ekleyerek başlayın.

2) Döngüde:

2a) Her grup için, ilk satırı tabloda kalan reset_value değerinin üzerinde olacak şekilde hesaplayın ve kimliği, çok büyük olan toplam toplamı ve geçici tabloda çok büyük olan bir önceki toplam toplamı saklayın.

2b) İlk geçici tablodaki satırları , ikinci geçici tabloda IDeşit veya daha küçük bir sonuç geçici tablosuna silin ID. Hareketli toplamı gerektiği gibi ayarlamak için diğer sütunları kullanın.

3) Silme işleminden sonra artık satırlar DELETE OUTPUTsonuç tablosuna bir ek çalıştırır . Bu, grubun sonundaki sıfırlama değerini asla aşmayan satırlar içindir.

Yukarıdaki algoritmanın bir uygulamasını T-SQL'de adım adım izleyeceğim.

Birkaç geçici tablo oluşturarak başlayın. #initial_resultsstandart veri toplamı ile orijinal verileri tutar, #group_bookkeepingher bir döngüye hangi satırların hareket ettirilebileceğini anlayacak şekilde güncellenir ve #final_resultssıfırlama için ayarlanan toplam çalışma toplamıyla sonuçları içerir.

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;

Sonra ekleme ve dizin oluşturma paralel olarak yapılabilir geçici tabloda kümelenmiş dizin oluşturun. Makinemde büyük bir fark yarattı ama seninkinde olmayabilir. Kaynak tabloda bir dizin oluşturmak yardımcı görünmüyordu, ancak bu makinenizde yardımcı olabilir.

Aşağıdaki kod döngüde çalışır ve defter tutma tablosunu günceller. Her grup için, IDsonuçlar tablosuna taşınması gereken maksimum değeri bulmamız gerekir. İlk satırdaki toplamdan çıkarabilmemiz için bu satırdan toplama ihtiyacımız var. A grp_doneiçin yapılacak başka iş olmadığında sütun 1 olarak ayarlanır 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);

Gerçekten LOOP JOINgenel olarak ipucu hayranı değil , ama bu basit bir sorgu ve istediğimi elde etmenin en hızlı yoluydu. Yanıt süresini gerçekten optimize etmek için DOP 1 birleştirme birleşimleri yerine paralel iç içe döngü birleşimleri istedim.

Aşağıdaki kod döngüde çalışır ve verileri ilk tablodan son sonuçlar tablosuna taşır. İlk çalışan toplam ayarına dikkat edin.

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;

Size kolaylık sağlamak için tam kod aşağıdadır:

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;

sadece harika sana ödül ile ödüllendireceğim
P ரதீப்

Sunucumuzda, 50000 grp ve 60 id için 1 dakika 10 saniye sürdü. Recursive CTE2 dakika 15 saniye sürdü
P ரதீப்

Her iki kodu da aynı verilerle test ettim. Senin harikaydı. Daha da geliştirilebilir mi?
P ரதீப்

Demek istediğim, kodunuzu gerçek verilerimizde çalıştırdım ve test ettim. Hesaplama gerçek prosedürümde geçici tablolarda işlenir, büyük olasılıkla sıkıca paketlenmelidir. 30 saniye civarında herhangi bir şeye indirgenebilirse iyi olur
P ரதீப்

@Prdp Bir güncelleme kullanan hızlı bir yaklaşım denedi, ancak daha kötü görünüyordu. Bir süre daha bu konulara bakamayacaksınız. Sunucunuzda en yavaş hangi parçanın çalıştığını anlayabilmeniz için her işlemin ne kadar sürdüğünü günlüğe kaydetmeyi deneyin. Bu kodu veya genel olarak daha iyi bir algoritmayı hızlandırmanın bir yolu kesinlikle mümkündür.
Joe Obbish

4

CURSOR Kullanma:

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

Buraya bakın: http://rextester.com/WSPLO95303


3

Pencereli değil, saf SQL sürümü:

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 lehçesinde uzman değilim. Bu PostrgreSQL için bir ilk sürümüdür (doğru anlıyorsam ben SQL Server özyineli bölümünde LIMIT 1 / TOP 1 kullanamazsınız):

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;

@JoeObbish dürüst olmak gerekirse, bu sorudan tamamen açık değil. Örneğin, beklenen sonuçlar grpsütun göstermez .
ypercubeᵀᴹ

@JoeObbish de anladım. ancak, soru bu konuda açık bir açıklamadan yararlanabilir. Sorudaki kod (CTE ile) da onu kullanmaz (ve hatta farklı adlandırılmış sütunlara sahiptir). Soruyu okuyan herkes için açıktır - diğer cevapları veya yorumları okumak zorunda kalmazlar ve yapmazlar.
ypercubeᵀᴹ

@ ypercubeᵀᴹ Soru üzerine gerekli bilgileri ekledi.
P ரதீப்

1

Soruna saldırmak için birkaç sorunuz / yönteminiz var, ancak bize vermediniz - hatta düşünmediniz mi? - tablodaki dizinler.

Tabloda hangi dizinler var? Bir yığın mı yoksa kümelenmiş bir dizini var mı?

Bu dizini ekledikten sonra önerilen çeşitli çözümleri denemek istiyorum:

(grp, id) INCLUDE (val, reset_val)

Veya yalnızca kümelenmiş dizini değiştirin (veya yapın) (grp, id).

Belirli bir sorguyu hedefleyen bir dizine sahip olmak, tüm yöntemlerin olmasa bile çoğunun verimliliğini artırmalıdır.


Soruya gerekli bilgiler eklendi.
P ரதீப்
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.