PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Tableau d'historique des stocks moyens

La difficulté spéciale de cette tâche :vous ne pouvez pas simplement sélectionner des points de données à l'intérieur de votre plage de temps, mais devez tenir compte des dernières point de données avant la plage de temps et le plus tôt point de données après la plage horaire en plus. Cela varie pour chaque ligne et chaque point de données peut exister ou non. Nécessite une requête sophistiquée et rend difficile l'utilisation des index.

Vous pouvez utiliser types de plage et opérateurs (Postgres 9.2+ ) pour simplifier les calculs :

WITH input(a,b) AS (SELECT '2013-01-01'::date  -- your time frame here
                         , '2013-01-15'::date) -- inclusive borders
SELECT store_id, product_id
     , sum(upper(days) - lower(days))                    AS days_in_range
     , round(sum(value * (upper(days) - lower(days)))::numeric
                    / (SELECT b-a+1 FROM input), 2)      AS your_result
     , round(sum(value * (upper(days) - lower(days)))::numeric
                    / sum(upper(days) - lower(days)), 2) AS my_result
FROM (
   SELECT store_id, product_id, value, s.day_range * x.day_range AS days
   FROM  (
      SELECT store_id, product_id, value
           , daterange (day, lead(day, 1, now()::date)
             OVER (PARTITION BY store_id, product_id ORDER BY day)) AS day_range 
      FROM   stock
      ) s
   JOIN  (
      SELECT daterange(a, b+1) AS day_range
      FROM   input
      ) x ON s.day_range && x.day_range
   ) sub
GROUP  BY 1,2
ORDER  BY 1,2;

Remarque, j'utilise le nom de colonne day au lieu de date . Je n'utilise jamais de noms de type de base comme noms de colonne.

Dans la sous-requête sub Je récupère le jour de la ligne suivante pour chaque élément avec la fonction de fenêtre lead() , en utilisant l'option intégrée pour fournir "aujourd'hui" par défaut là où il n'y a pas de ligne suivante.
Avec cela, je forme une daterange et faites-le correspondre à l'entrée avec l'opérateur de chevauchement && , en calculant la plage de dates résultante avec l'opérateur d'intersection * .

Toutes les gammes ici sont avec exclusif bordure supérieure. C'est pourquoi j'ajoute un jour à la plage d'entrée. De cette façon, nous pouvons simplement soustraire lower(range) de upper(range) pour obtenir le nombre de jours.

Je suppose que "hier" est le dernier jour avec des données fiables. "Aujourd'hui" peut encore changer dans une application réelle. Par conséquent, j'utilise "aujourd'hui" (now()::date ) comme bordure supérieure exclusive pour les plages ouvertes.

Je fournis deux résultats :

  • your_result correspond aux résultats affichés.
    Vous divisez par le nombre de jours dans votre plage de dates sans condition. Par exemple, si un article n'est répertorié que le dernier jour, vous obtenez une "moyenne" très faible (trompeuse !)

  • my_result calcule des nombres identiques ou supérieurs.
    Je divise par le réel nombre de jours pendant lesquels un article est répertorié. Par exemple, si un article n'est répertorié que pour le dernier jour, je renvoie la valeur répertoriée comme moyenne.

Pour donner un sens à la différence, j'ai ajouté le nombre de jours pendant lesquels l'article était répertorié :days_in_range

SQL Fiddle .

Indice et performances

Pour ce type de données, les anciennes lignes ne changent généralement pas. Cela constituerait un excellent cas pour une vue matérialisée :

CREATE MATERIALIZED VIEW mv_stock AS
SELECT store_id, product_id, value
     , daterange (day, lead(day, 1, now()::date) OVER (PARTITION BY store_id, product_id
                                                       ORDER BY day)) AS day_range
FROM   stock;

Ensuite, vous pouvez ajouter un index GiST qui prend en charge l'opérateur pertinent && :

CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);

Grand cas de test

J'ai exécuté un test plus réaliste avec 200 000 lignes. La requête utilisant le MV était environ 6 fois plus rapide, ce qui était environ 10 fois plus rapide que la requête de @Joop. Les performances dépendent fortement de la distribution des données. Un MV aide le plus avec de grandes tables et une fréquence élevée d'entrées. De plus, si la table contient des colonnes qui ne sont pas pertinentes pour cette requête, un MV peut être plus petit. Une question de coût contre gain.

J'ai mis toutes les solutions publiées jusqu'à présent (et adaptées) dans un gros violon pour jouer avec :

SQL Fiddle avec un gros cas de test.
SQL Fiddle avec seulement 40 000 lignes - pour éviter le timeout sur sqlfiddle.com