Ceci est une question vraiment amusante (demandée par SQL Server) et je voulais l'essayer de voir comment cela a été fait dans PostgreSQL. Voyons si quelqu'un d'autre peut mieux le faire. Prendre ces données,
CREATE TABLE foo
AS
SELECT pkid::int, numvalue::int, groupid::int
FROM ( VALUES
( 1, -1 , 1 ),
( 2, -2 , 1 ),
( 3, 5 , 1 ),
( 4, -7 , 1 ),
( 5, 1 , 2 )
) AS t(pkid, numvalue, groupid);
Nous essayons de générer ceci:
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
Le problème est décrit comme étant donné que
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 d'addition. Si le quatrième numéro change 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.
Leurs solutions utilisent tous de la récursivité.
Nous utilisons CREATE FUNCTION
Pour créer une fonction int_add_pos_or_zero
qui ajoute des chiffres, mais s'ils sont inférieurs à 0, retourne 0.
CREATE FUNCTION int_add_pos_or_zero(int, int)
RETURNS int
AS $$
BEGIN
RETURN greatest($1 + $2, 0);
END;
$$
LANGUAGE plpgsql
IMMUTABLE;
Maintenant nous CREATE AGGREGATE
à ce sujet afin que nous puissions l'exécuter dans une fonction de fenêtre. Nous définissons INITCOND
pour être =0
.
CREATE AGGREGATE add_pos_or_zero(int) (
SFUNC = int_add_pos_or_zero,
STYPE = int,
INITCOND = 0
);
Maintenant, nous l'interrogeons comme n'importe quelle autre Fonction de fenêtre.
SELECT pkid,
groupid,
numvalue,
add_pos_or_zero(numvalue) OVER (PARTITION BY groupid ORDER BY pkid)
FROM foo;
pkid | groupid | numvalue | add_pos_or_zero
------+---------+----------+-----------------
1 | 1 | -1 | 0
2 | 1 | -2 | 0
3 | 1 | 5 | 5
4 | 1 | -7 | 0
5 | 2 | 1 | 1
(5 rows)
C'est un peu comme requête intelligente de dnoeth (même logique de base). Juste plus court et plus efficace avec une expression plus simple dans la requête extérieure:
SELECT groupid, pkid
, simple_sum
- LEAST(MIN(simple_sum)
OVER (PARTITION BY groupid
ORDER BY pkid ROWS UNBOUNDED PRECEDING), 0) AS rolling_sum
FROM (
SELECT pkid, numvalue, groupid
, SUM(numvalue) OVER (PARTITION BY groupid
ORDER BY pkid ROWS UNBOUNDED PRECEDING) AS simple_sum
FROM foo
) sub;
Comment ça marche?
[. C'est précisément ce que le calcul de l'extérieur SELECT
fait, soustrayez des nombres négatifs ajoute le positif correspondant:
LEAST(MIN(simple_sum) OVER (PARTITION BY groupid
ORDER BY pkid ROWS UNBOUNDED PRECEDING), 0)
L'environnement LEAST
annule toute action pour des nombres positifs (ou 0). Le moins négatif (le plus grand nombre absolu) de la simple somme en cours est ce que nous devons ajouter jusqu'à présent au total. Chaque fois que notre calcul irait en dessous de zéro, nous obtenons un nouveau absolu bas dans la simple somme en cours. Tout est magnifiquement simple.
Basé sur mise en œuvre d'Abelisto , amélioré:
CREATE OR REPLACE FUNCTION f_special_rolling_sum()
RETURNS TABLE (groupid int, pkid int, numvalue int, rolling_sum int) AS
$func$
DECLARE
last_groupid int;
BEGIN
FOR groupid, pkid, numvalue IN
SELECT f.groupid, f.pkid, f.numvalue
FROM foo f
ORDER BY f.groupid, f.pkid
LOOP
IF last_groupid = groupid THEN -- same partition continues
rolling_sum := GREATEST(rolling_sum + numvalue, 0);
ELSE -- new partition
last_groupid := groupid;
rolling_sum := GREATEST(numvalue, 0);
END IF;
RETURN NEXT;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Appel:
SELECT * FROM f_special_rolling_sum();
Toutes les solutions fournies jusqu'à présent peuvent bénéficier d'une analyse à l'index qu'avec un indice de couverture:
CREATE INDEX idx_foo_covering ON foo(groupid, pkid, numvalue);
En rapport:
Après avoir optimisé la fonction, les requêtes, l'index (et le test lui-même), je vois des performances similaires pour les deux, la requête étant légèrement plus rapide que la fonction. (La fonction d'agrégat un peu plus lente que le reste.) Suite de tests étendue (basée sur violon d'Abelisto ):
dbfiddle pour pg 9.6 ICI
dbfiddle pour pg 10 ICI