web-dev-qa-db-fra.com

Réglez le sol non négatif pour la somme de roulement, dans SQL Server

J'ai besoin de régler un étage sur le calcul de la somme roulante. Par exemple, avec

PKID    NumValue    GroupID
----------------------------
1       -1          1
2       -2          1
3       5           1
4       -7          1
5       1           2

Je voudrais avoir:

PKID   RollingSum    GroupID
----------------------------- ## Explanation: 
1      0             1        ## 0 - 1 < 0  => 0
2      0             1        ## 0 - 2 < 0  => 0
3      5             1        ## 0 + 5 > 0  => 5
4      0             1        ## 5 - 7 < 0  => 0

Lorsque l'ajout d'un nombre négatif entraînera la somme négative, la limite sera activée pour définir le résultat comme zéro. L'ajout ultérieure devrait être basé sur cette valeur ajustée, au lieu de la somme de roulement d'origine.

(( le résultat attendu doit être atteint à l'aide de l'addition. Si le quatrième numéro passe de -7 à -3, le quatrième résultat doit être 2 au lieu de 0

Si une somme unique peut être fournie plutôt que quelques numéros de roulement, il serait également acceptable. Je peux utiliser des procédures stockées pour mettre en œuvre un ajout non négatif, mais ce serait trop bas niveau.

Le problème de la vie réelle est que nous enregistrons la commande placée comme une quantité positive et annulée comme négative. En raison des problèmes de connectivité Les clients peuvent cliquer sur le bouton cancel plus d'une fois, ce qui entraînera une valeur de plusieurs valeurs négatives. Lors du calcul de nos revenus, "zéro" doit être une limite pour les ventes.


Cette application d'entreprise est absolument stupide, mais rien que je puisse faire à ce sujet. Pour cette question, veuillez seulement envisager une solution pouvant être utilisée par un DBA.

J'attends une cinquantaine de rangées par GroupID au plus.

5
Ryan

Voici un exemple de CTE récursif que j'ai proposé (ce qui semble fonctionner). Utilise Row_Number () sur pour créer un numéro de séquence sans lacunes. Je ne sais pas à quel point cela fonctionnerait avec vos données, mais c'est quelque chose à essayer.

--Set up demo data
IF OBJECT_ID('tempdb..#temp') IS NOT NULL
    drop table #temp
go
create table #temp (PKID int, NumValue int, GroupID int)
insert into #temp values
(1,-1,1), (3,-2,1), (5,5,1), (7,-3,1), (9,1,2)

--here is the real code
;
with RowNumberAddedToTemp as 
(
SELECT 
  ROW_NUMBER() OVER(ORDER BY PKID ASC) AS rn,
  * from #temp
  )
,x
AS (
    SELECT PKID         --Anchor row
        ,NumValue
        ,RunningTotal = CASE 
            WHEN NumValue < 0   --if initial value less than zero, make zero
                THEN 0
            ELSE NumValue
            END
        ,GroupID
        ,rn
    FROM RowNumberAddedToTemp
    WHERE rn = 1      

    UNION ALL

    SELECT y.PKID
        ,y.NumValue
        ,CASE 
            WHEN x.GroupID <> y.groupid     --did GroupId change?
                THEN CASE 
                        WHEN y.NumValue < 0     --if value is less than zero, make zero
                            THEN 0
                        ELSE y.numvalue         --start new groupid totals
                        END
            WHEN x.RunningTotal + y.NumValue < 0    --If adding the current row makes the total < 0, make zero
                THEN 0
            ELSE x.RunningTotal + y.NumValue        --Add to the running total for the current groupid
            END
        ,y.Groupid
        ,y.rn
    FROM x
    INNER JOIN RowNumberAddedToTemp AS y ON y.rn = x.rn + 1
    )
SELECT PKID
    ,Numvalue
    ,RunningTotal
    ,GroupID
FROM x
ORDER BY PKID
OPTION (MAXRECURSION 10000);
3
Scott Hodgin

Vous trouverez ci-dessous une solution récursive similaire à Scott Hodgin's Réponse , mais cela devrait être capable de mieux tirer parti d'un index. Cela améliorera les performances en fonction de vos données. Je me suis moqué d'un million de rangées de données avec des groupes de 20000. Chacun a 50 rangées associées à celle-ci:

DROP TABLE IF EXISTS biz_application_problems;
CREATE TABLE dbo.biz_application_problems (
    PKID BIGINT NOT NULL,
    NumValue INT NOT NULL,
    GroupID INT NOT NULL,
    PRIMARY KEY (PKID)
);

INSERT INTO dbo.biz_application_problems WITH (TABLOCK)
SELECT TOP (1000000) 
  2 * ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, CAST(CRYPT_GEN_RANDOM(1) AS INT) % 22 - 11
, 1 + (-1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))) / 50
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE INDEX gotta_go_fast ON dbo.biz_application_problems (GroupID, PKID)
    INCLUDE (NumValue);

L'indice non clusterié que j'ai ajouté à la table devrait permettre à la partie récursive de la CTE d'obtenir la ligne suivante avec une seule recherche d'un indice. Cela signifie également que l'ordre des données dans la table en fonction du PK n'a pas d'importance. L'autre astuce ici est d'utiliser ROW_NUMBER() pour obtenir efficacement la ligne suivante. Ceci est nécessaire car TOP ne peut pas être utilisé dans la partie récursive d'une CTE. Voici la requête:

WITH rec_cte AS (
    SELECT TOP 1
      GroupID
    , PKID
    , CASE WHEN NumValue < 0 THEN 0 ELSE NumValue END RunningSum
    , NumValue -- for validation purposes only
    FROM dbo.biz_application_problems
    ORDER BY GroupId, PKID

    UNION ALL

    SELECT t.GroupID
    , t.PKID
    , t.RunningSum
    , t.NumValue
    FROM 
    (
        SELECT 
          b.GroupID
        , b.PKID
        , CASE WHEN ca.RunningSum < 0 THEN 0 ELSE ca.RunningSum END RunningSum
        , b.NumValue
        , ROW_NUMBER() OVER (ORDER BY b.GroupId, b.PKID) rn
        FROM dbo.biz_application_problems b
        INNER JOIN rec_cte r ON
            (b.GroupID = r.GroupID AND b.PKID > r.PKID) OR b.GroupID > r.GroupID
        CROSS APPLY (
            SELECT CASE WHEN b.GroupID <> r.GroupID THEN 0 ELSE r.RunningSum END + b.NumValue
        ) ca (RunningSum)
    ) t
    WHERE t.rn = 1
)
SELECT *
FROM rec_cte
OPTION (MAXRECURSION 0);

Voici un échantillon des résultats:

╔═════════╦══════╦════════════╦══════════╗
║ GroupID ║ PKID ║ RunningSum ║ NumValue ║
╠═════════╬══════╬════════════╬══════════╣
║       7 ║  700 ║         13 ║       -4 ║
║       8 ║  702 ║          0 ║       -2 ║
║       8 ║  704 ║          7 ║        7 ║
║       8 ║  706 ║          8 ║        1 ║
║       8 ║  708 ║          3 ║       -5 ║
║       8 ║  710 ║          0 ║       -4 ║
║       8 ║  712 ║          0 ║       -7 ║
║       8 ║  714 ║          7 ║        7 ║
║       8 ║  716 ║          2 ║       -5 ║
║       8 ║  718 ║         10 ║        8 ║
║       8 ║  720 ║          0 ║      -11 ║
║       8 ║  722 ║          0 ║       -7 ║
╚═════════╩══════╩════════════╩══════════╝

Sur ma machine, le code prend environ 10 secondes pour traiter un million de lignes. Nous pouvons voir l'index recherche qui retourne une seule rangée pour chaque itération:

seeks

J'ai téléchargé le plan actuel ici aussi.

1
Joe Obbish