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.
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);
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:
J'ai téléchargé le plan actuel ici aussi.