web-dev-qa-db-fra.com

Définir le sol non négatif pour la somme de roulement, dans PostgreSQL

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é.

3
Evan Carroll

Utilisation d'une fonction d'agrégation personnalisée

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)
3
Evan Carroll

Requête Sith Fenêtre Fonctions

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.

Fonctions PL/PGSQL

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();

Indice

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:

Tests

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

2
Erwin Brandstetter