Réinitialiser le total en cours basé sur une autre colonne


10

J'essaie de calculer le total cumulé. Mais il devrait se réinitialiser lorsque la somme cumulée supérieure à une autre valeur de colonne

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

Détails de l'index:

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

exemples de données

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

Résultat attendu

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

Requete:

J'ai obtenu le résultat en utilisant Recursive CTE. La question d'origine est ici /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 

Existe-t-il une meilleure alternative T-SQLsans utiliser CLR.?


Mieux comment? Cette requête présente-t-elle de mauvaises performances? En utilisant quelles métriques?
Aaron Bertrand

@AaronBertrand - Pour une meilleure compréhension, j'ai publié des exemples de données pour un seul groupe. Je dois faire la même chose pour les 50000groupes avec des 60 identifiants . le nombre total d'enregistrements sera donc d'environ 3000000. Je suis sûr que Recursive CTEcela ne sera pas bien adapté 3000000. Mettra à jour les mesures à mon retour au bureau. Pouvons-nous y parvenir en utilisant sum()Over(Order by)comme vous l'avez utilisé dans cet article sqlperformance.com/2012/07/07/t-sql-queries/running-totals
P ரதீப்

Un curseur pourrait faire mieux qu'un CTE récursif
paparazzo

Réponses:


6

J'ai examiné des problèmes similaires et je n'ai jamais été en mesure de trouver une solution de fonction de fenêtre qui effectue un seul passage sur les données. Je ne pense pas que ce soit possible. Les fonctions de fenêtre doivent pouvoir être appliquées à toutes les valeurs d'une colonne. Cela rend les calculs de réinitialisation comme celui-ci très difficiles, car une réinitialisation modifie la valeur de toutes les valeurs suivantes.

Une façon de penser au problème est que vous pouvez obtenir le résultat final souhaité si vous calculez un total cumulé de base tant que vous pouvez soustraire le total cumulé de la ligne précédente correcte. Par exemple, dans vos exemples de données, la valeur de id4 est le running total of row 4 - the running total of row 3. La valeur de id6 est le running total of row 6 - the running total of row 3car une réinitialisation n'a pas encore eu lieu. La valeur de id7 est le running total of row 7 - the running total of row 6et ainsi de suite.

J'aborderais cela avec T-SQL en boucle. Je me suis un peu emporté et je pense avoir une solution complète. Pour 3 millions de lignes et 500 groupes, le code s'est terminé en 24 secondes sur mon bureau. Je teste avec SQL Server 2016 Developer Edition avec 6 vCPU. Je profite des insertions parallèles et de l'exécution parallèle en général, vous devrez donc peut-être modifier le code si vous utilisez une version antérieure ou si vous avez des limitations DOP.

Ci-dessous le code que j'ai utilisé pour générer les données. Les plages VALet RESET_VALdoivent être similaires à vos exemples de données.

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;

L'algorithme est le suivant:

1) Commencez par insérer toutes les lignes avec un total cumulé standard dans une table temporaire.

2) En boucle:

2a) Pour chaque groupe, calculez la première ligne avec un total cumulé au-dessus de la valeur reset_value restante dans la table et stockez l'id, le total cumulé qui était trop grand et le total cumulé précédent qui était trop grand dans une table temporaire.

2b) Supprimez les lignes de la première table temporaire dans une table temporaire de résultats dont la valeur est IDinférieure ou égale à celle IDde la seconde table temporaire. Utilisez les autres colonnes pour ajuster le total cumulé selon vos besoins.

3) Après que la suppression ne traite plus les lignes, exécutez-en une supplémentaire DELETE OUTPUTdans le tableau des résultats. Il s'agit des lignes à la fin du groupe qui ne dépassent jamais la valeur de réinitialisation.

Je vais passer en revue une implémentation de l'algorithme ci-dessus dans T-SQL étape par étape.

Commencez par créer quelques tables temporaires. #initial_resultscontient les données d'origine avec le total cumulé standard, #group_bookkeepingest mise à jour à chaque boucle pour déterminer quelles lignes peuvent être déplacées et #final_resultscontient les résultats avec le total cumulé ajusté pour les réinitialisations.

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;

Je crée l'index cluster sur la table temporaire après pour que l'insertion et la construction de l'index puissent se faire en parallèle. Cela a fait une grande différence sur ma machine mais peut-être pas sur la vôtre. La création d'un index sur la table source n'a pas semblé aider mais cela pourrait aider sur votre machine.

Le code ci-dessous s'exécute dans la boucle et met à jour la table de comptabilité. Pour chaque groupe, nous devons obtenir la recherche du maximum IDqui doit être déplacé dans le tableau des résultats. Nous avons besoin du total cumulé de cette ligne pour pouvoir le soustraire du total cumulé initial. La grp_donecolonne est définie sur 1 lorsqu'il n'y a plus de travail à faire pour a 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);

Vraiment pas un fan de l' LOOP JOINindice en général, mais c'est une requête simple et c'était le moyen le plus rapide d'obtenir ce que je voulais. Pour vraiment optimiser le temps de réponse, je voulais des jointures de boucles imbriquées parallèles au lieu de jointures de fusion DOP 1.

Le code ci-dessous s'exécute dans la boucle et déplace les données de la table initiale vers la table de résultats finale. Remarquez l'ajustement du total cumulé initial.

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;

Pour votre commodité, voici le code complet:

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;

tout simplement génial, je vais vous récompenser avec une prime
P ரதீப்

Dans notre serveur, pour 50000 grp et 60 id, le vôtre a pris 1 minute et 10 secondes. Recursive CTEa pris 2 minutes et 15 secondes
P ரதீப்

J'ai testé les deux codes avec les mêmes données. Le vôtre était génial. Peut-il être encore amélioré?
P ரதீப்

Je veux dire, j'ai exécuté votre code sur nos données réelles et l'ai testé. Le calcul est traité dans des tables temporaires dans ma procédure réelle, très probablement il devrait être bien emballé. Ce sera bien s'il peut être réduit à quelque chose autour de 30 secondes
P ரதீப்

@Prdp A essayé une approche rapide qui utilisait une mise à jour mais cela semblait être pire. Je ne pourrai plus examiner cela pendant un certain temps. Essayez de consigner la durée de chaque opération afin de savoir quelle partie s'exécute le plus lentement sur votre serveur. Il est certainement possible qu'il existe un moyen d'accélérer ce code ou un meilleur algorithme en général.
Joe Obbish

4

Utilisation d'un CURSEUR:

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

Vérifiez ici: http://rextester.com/WSPLO95303


3

Version non fenêtrée, mais pure 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
;

Je ne suis pas un spécialiste du dialecte de SQL Server. Il s'agit d'une version initiale pour PostrgreSQL (si je comprends bien je ne peux pas utiliser LIMIT 1 / TOP 1 en partie récursive dans 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;

@JoeObbish pour être honnête, ce n'est pas tout à fait clair de la question. Les résultats attendus, par exemple, ne montrent aucune grpcolonne.
ypercubeᵀᴹ

@JoeObbish, c'est aussi ce que j'ai compris. pourtant, la question pourrait bénéficier d'une déclaration explicite à ce sujet. Le code de la question (avec le CTE) ne l'utilise pas non plus (et il a même des colonnes nommées différemment). Il serait évident pour quiconque lit la question - ils n'auraient pas - et ne devraient pas - avoir à lire les autres réponses ou commentaires.
ypercubeᵀᴹ

@ ypercubeᵀᴹ Ajout des informations requises sur la question.
P ரதீப்

1

Il semble que vous ayez plusieurs requêtes / méthodes pour attaquer le problème, mais vous ne nous avez pas fourni - ou même envisagé? - les index sur la table.

Quels index y a-t-il dans le tableau? Est-ce un tas ou a-t-il un index clusterisé?

J'essaierais les différentes solutions suggérées après avoir ajouté cet index:

(grp, id) INCLUDE (val, reset_val)

Ou tout simplement changer (ou faire) l'index cluster pour être (grp, id).

Avoir un index qui cible la requête spécifique devrait améliorer l'efficacité - de la plupart des méthodes sinon de toutes.


Ajout des informations requises sur la question.
P ரதீப்
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.