Zresetuj sumę bieżącą na podstawie innej kolumny

10

Próbuję obliczyć sumę bieżącą. Ale powinien się zresetować, gdy suma skumulowana jest większa niż inna wartość kolumny

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

Szczegóły indeksu:

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

przykładowe dane

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

Spodziewany wynik

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

Pytanie:

Mam wynik przy użyciu Recursive CTE. Oryginalne pytanie jest tutaj /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 

Czy jest jakaś lepsza alternatywa T-SQLbez używania CLR.?

P ரதீப்
źródło
Lepiej jak? Czy to zapytanie wykazuje słabą wydajność? Korzystasz z jakich danych?
Aaron Bertrand
@AaronBertrand - Dla lepszego zrozumienia opublikowałem przykładowe dane tylko dla jednej grupy. To samo muszę zrobić dla 50000grup z 60 Id . więc całkowita liczba rekordów będzie w pobliżu 3000000. Na pewno Recursive CTEnie będzie dobrze skalować 3000000. Zaktualizuję wskaźniki, kiedy wrócę do biura. Czy możemy to osiągnąć za pomocą tego, sum()Over(Order by)którego użyłeś w tym artykule sqlperformance.com/2012/07/t-sql-queries/running-totals
P ரதீப்
Kursor może być lepszy niż rekurencyjne CTE
paparazzo

Odpowiedzi:

6

Patrzyłem na podobne problemy i nigdy nie byłem w stanie znaleźć rozwiązania funkcji okna, które wykonuje jedno przejście przez dane. Nie sądzę, żeby to było możliwe. Funkcje okna muszą być możliwe do zastosowania do wszystkich wartości w kolumnie. To bardzo utrudnia obliczenia resetowania, ponieważ jeden reset zmienia wartość wszystkich poniższych wartości.

Jednym ze sposobów myślenia o problemie jest to, że możesz uzyskać pożądany wynik końcowy, jeśli obliczysz podstawową sumę bieżącą, o ile możesz odjąć sumę bieżącą od poprawnego poprzedniego wiersza. Na przykład w przykładowych danych wartością id4 jest running total of row 4 - the running total of row 3. Wartość id6 wynika z tego, running total of row 6 - the running total of row 3że reset jeszcze się nie odbył. Wartość id7 jest taka sama running total of row 7 - the running total of row 6.

Podchodziłbym do tego z T-SQL w pętli. Trochę mnie poniosło i myślę, że mam pełne rozwiązanie. Dla 3 milionów wierszy i 500 grup kod zakończył się w ciągu 24 sekund na moim pulpicie. Testuję z SQL Server 2016 Developer Edition z 6 vCPU. Korzystam z równoległych wstawek i równoległego wykonywania, więc być może będziesz musiał zmienić kod, jeśli używasz starszej wersji lub masz ograniczenia DOP.

Poniżej kodu, którego użyłem do wygenerowania danych. Zakresy VALi RESET_VALpowinny być podobne do danych przykładowych.

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;

Algorytm wygląda następująco:

1) Zacznij od wstawienia wszystkich wierszy ze standardową sumą bieżącą do tabeli tymczasowej.

2) W pętli:

2a) Dla każdej grupy obliczyć pierwszy wiersz z sumą bieżącą powyżej wartości_resetu pozostającą w tabeli i zapisać identyfikator, sumę roboczą, która była zbyt duża, i poprzednią sumę roboczą, która była zbyt duża w tabeli tymczasowej.

2b) Usuń wiersze z pierwszej tabeli tymczasowej do tabeli wyników tymczasowej, która ma wartość IDmniejszą lub równą IDdrugiej tabeli tymczasowej. Użyj innych kolumn, aby dostosować bieżącą sumę według potrzeb.

3) Po usunięciu nie przetwarza już wierszy uruchom dodatkową DELETE OUTPUTtabelę wyników. Dotyczy to wierszy na końcu grupy, które nigdy nie przekraczają wartości resetowania.

Przejdę krok po kroku przez jedną implementację powyższego algorytmu w języku T-SQL.

Zacznij od utworzenia kilku tabel tymczasowych. #initial_resultsprzechowuje oryginalne dane ze standardową sumą bieżącą, #group_bookkeepingjest aktualizowany w każdej pętli, aby dowiedzieć się, które wiersze można przenieść, i #final_resultszawiera wyniki z sumą bieżącą skorygowaną dla resetowania.

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;

Po utworzeniu indeksu klastrowego w tabeli tymczasowej wstawianie i budowanie indeksu można wykonywać równolegle. Zrobiłem dużą różnicę na moim komputerze, ale może nie na twoim. Tworzenie indeksu w tabeli źródłowej nie wydawało się pomocne, ale może pomóc na twoim komputerze.

Poniższy kod działa w pętli i aktualizuje tabelę księgowości. Dla każdej grupy musimy znaleźć maksimum, IDktóre należy przenieść do tabeli wyników. Potrzebujemy sumę bieżącą z tego wiersza, abyśmy mogli odjąć ją od początkowej sumy bieżącej. grp_doneKolumna jest ustawiona na 1, gdy nie ma nic więcej do zrobienia dla 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);

Naprawdę nie LOOP JOINjestem fanem podpowiedzi w ogóle, ale jest to proste zapytanie i był to najszybszy sposób na uzyskanie tego, czego chciałem. Aby naprawdę zoptymalizować czas odpowiedzi, chciałem złączeń równoległych zagnieżdżonych zamiast połączeń scalających DOP 1.

Poniższy kod działa w pętli i przenosi dane z początkowej tabeli do końcowej tabeli wyników. Zwróć uwagę na korektę początkowej sumy bieżącej.

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;

Dla Twojej wygody poniżej znajduje się pełny kod:

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;
Joe Obbish
źródło
po prostu niesamowite
przyznam
Na naszym serwerze, dla 50000 GRP i 60 ID Twój zajął 1 minutę i 10 sekund. Recursive CTEzajęło 2 minuty i 15 sekund
P ரதீப்
Przetestowałem oba kody z tymi samymi danymi. Twój był niesamowity. Czy można to jeszcze poprawić?
P ரதீப்
Miałem na myśli, że uruchomiłem twój kod na naszych prawdziwych danych i przetestowałem go. Obliczenia są przetwarzane w tabelach tymczasowych w mojej prawdziwej procedurze, najprawdopodobniej powinny być ciasno zapakowane. Dobrze będzie, jeśli uda się go zredukować do około 30 sekund
P ரதீப்
@Prdp Próbowałem szybkiego podejścia, które wykorzystało aktualizację, ale wydawało się, że jest gorzej. Przez jakiś czas nie będziemy w stanie przyjrzeć się temu więcej. Spróbuj zalogować się, ile czasu zajmuje każda operacja, aby dowiedzieć się, która część działa najwolniej na twoim serwerze. Jest zdecydowanie możliwe, że istnieje sposób na przyspieszenie tego kodu lub ogólnie lepszy algorytm.
Joe Obbish
4

Korzystanie z KURSORA:

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

Sprawdź tutaj: http://rextester.com/WSPLO95303

McNets
źródło
3

Wersja bez okien, ale w czystej wersji 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
;

Nie jestem specjalistą w dialekcie SQL Server. To jest początkowa wersja PostrgreSQL (jeśli dobrze rozumiem, nie mogę użyć LIMIT 1 / TOP 1 w części rekurencyjnej w 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;
Roman Tkachuk
źródło
@JoeObbish szczerze mówiąc, nie jest to całkowicie jasne z pytania. Na przykład oczekiwane wyniki nie pokazują grpkolumny.
ypercubeᵀᴹ
@JoeObbish to też zrozumiałem. jednak pytanie mogłoby skorzystać z wyraźnego oświadczenia na ten temat. Kod w pytaniu (z CTE) również go nie używa (a nawet ma kolumny o różnych nazwach). Byłoby oczywiste dla każdego, kto czyta pytanie - nie musieliby - i nie powinni - czytać innych odpowiedzi lub komentarzy.
ypercubeᵀᴹ
@ ypercubeᵀᴹ Dodano wymagane informacje do pytania.
P ரதீப்
1

Wygląda na to, że masz kilka zapytań / metod, aby zaatakować problem, ale nie dostarczyłeś nam - a nawet zastanowiłeś się? - indeksy na stole.

Jakie indeksy są w tabeli? Czy to jest kupa, czy ma indeks klastrowany?

Wypróbowałbym różne rozwiązania sugerowane po dodaniu tego indeksu:

(grp, id) INCLUDE (val, reset_val)

Lub po prostu zmień (lub stwórz) indeks klastrowany (grp, id).

Posiadanie indeksu ukierunkowanego na określone zapytanie powinno poprawić wydajność - większości, jeśli nie wszystkich metod.

ypercubeᵀᴹ
źródło
Dodano wymagane informacje do pytania.
P ரதீப்