web-dev-qa-db-fra.com

Somme glissante / nombre / moyenne sur l'intervalle de dates

Dans une base de données de transactions couvrant des milliers d'entités sur 18 mois, je voudrais exécuter une requête pour regrouper toutes les périodes de 30 jours possibles par entity_id Avec une SOMME de leurs montants de transaction et COUNT de leurs transactions dans ce Période de 30 jours et renvoyer les données d'une manière que je peux ensuite interroger. Après de nombreux tests, ce code accomplit une grande partie de ce que je veux:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

Et je vais utiliser dans une requête plus large structurée quelque chose comme:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

Le cas que cette requête ne couvre pas, c'est quand le nombre de transactions s'étendra sur plusieurs mois, mais sera toujours dans les 30 jours les uns des autres. Ce type de requête est-il possible avec Postgres? Si c'est le cas, je salue toute contribution. De nombreux autres sujets traitent des agrégats " en cours d'exécution ", et non du roulement .

Mise à jour

Le script CREATE TABLE:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

Des exemples de données peuvent être trouvés ici . J'utilise PostgreSQL 9.1.16.

La sortie idéale comprendrait SUM(amount) et COUNT() de toutes les transactions sur une période continue de 30 jours. Voir cette image, par exemple:

Example of rows that would ideally be included in a "set" but are not because my set is static by month.

Le surlignage de la date verte indique ce qui est inclus par ma requête. La surbrillance de la ligne jaune indique les enregistrements de ce que j'aimerais faire partie de l'ensemble.

Lecture précédente:

22
tufelkinder

La requête que vous avez

Vous pouvez simplifier votre requête à l'aide d'une clause WINDOW, mais cela ne fait que raccourcir la syntaxe, sans changer le plan de requête.

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • Vous utilisez également la count(*) légèrement plus rapide, puisque id est certainement défini NOT NULL?
  • Et vous n'avez pas besoin de ORDER BY entity_id Puisque vous avez déjà PARTITION BY entity_id

Vous pouvez encore simplifier:
N'ajoutez pas du tout ORDER BY À la définition de la fenêtre, cela ne correspond pas à votre requête. Ensuite, vous n'avez pas besoin de définir un cadre de fenêtre personnalisé, soit:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

Plus simple, plus rapide, mais toujours juste une meilleure version de ce que vous avez, avec statique mois.

La requête que vous voudrez peut-être

... n'est pas clairement défini, je vais donc construire sur ces hypothèses:

Comptez les transactions et le montant pour chaque période de 30 jours dans la première et la dernière transaction de tout entity_id. Exclure les périodes de début et de fin sans activité, mais inclure toutes les périodes de 30 jours possibles dans ces limites extérieures.

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

Ceci répertorie toutes les périodes de 30 jours pour chaque entity_id Avec vos agrégats et avec trans_date Étant le premier jour (incl.) De la période. Pour obtenir des valeurs pour chaque ligne individuelle, joignez à nouveau la table de base ...

La difficulté de base est la même que celle discutée ici:

La définition du cadre d'une fenêtre ne peut pas dépendre des valeurs de la ligne actuelle.

Et appelez plutôt generate_series() avec timestamp input:

La requête que vous voulez réellement

Après la mise à jour des questions et la discussion:
Accumulez des lignes du même entity_id Dans une fenêtre de 30 jours à partir de chaque transaction réelle.

Etant donné que vos données sont réparties de manière clairsemée, il devrait être plus efficace d'exécuter une auto-jointure avec une condition de plage , d'autant plus que Postgres 9.1 n'a pas LATERAL rejoint, pourtant:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

SQL Fiddle.

Une fenêtre mobile ne pouvait avoir de sens (en termes de performances) qu'avec des données pour la plupart des jours.

Cela fait pas agréger les doublons sur (trans_date, entity_id) Par jour, mais toutes les lignes du même jour sont toujours incluses dans la fenêtre de 30 jours.

Pour une grande table, un indice de couverture comme celui-ci pourrait aider un peu:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

La dernière colonne amount n'est utile que si vous en obtenez des analyses indexées uniquement. Sinon, laissez tomber.

Mais cela ne sera pas utilisé pendant que vous sélectionnez la table entière de toute façon. Il prendrait en charge les requêtes pour un petit sous-ensemble.

29
Erwin Brandstetter