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 .
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:
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:
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);
count(*)
légèrement plus rapide, puisque id
est certainement défini NOT NULL
?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.
... 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:
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
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.