Somma progressiva / conteggio / media nell'intervallo di date


20

In un database di transazioni che coprono migliaia di entità in 18 mesi, vorrei eseguire una query per raggruppare ogni possibile periodo di 30 giorni entity_idcon una SOMMA degli importi delle transazioni e COUNT delle loro transazioni in quel periodo di 30 giorni, e restituire i dati in modo che io possa quindi interrogare. Dopo molti test, questo codice realizza molto di ciò che voglio:

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;

E userò in una query più ampia strutturata qualcosa del tipo:

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;

Il caso in cui questa query non copre è quando il conteggio delle transazioni si estenderebbe per più mesi, ma comunque entro 30 giorni l'uno dall'altro. Questo tipo di query è possibile con Postgres? In tal caso, accolgo con favore qualsiasi input. Molti degli altri argomenti trattano aggregati "in esecuzione ", non a rotazione .

Aggiornare

La CREATE TABLEsceneggiatura:

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

I dati di esempio sono disponibili qui . Sto eseguendo PostgreSQL 9.1.16.

La produzione ideale dovrebbe includere SUM(amount)e COUNT()di tutte le transazioni su un periodo di 30 giorni continui. Vedi questa immagine, ad esempio:

Esempio di righe che sarebbero idealmente incluse in un "set" ma non perché il mio set è statico per mese.

L'evidenziazione della data verde indica ciò che viene incluso dalla mia query. L'evidenziazione della riga gialla indica che cosa vorrei far parte del set.

Lettura precedente:


1
Con every possible 30-day period by entity_idsi intende il periodo che può iniziare qualsiasi giorno, quindi 365 possibili periodi in un anno (non bisestile)? O vuoi considerare solo i giorni con una transazione effettiva come l'inizio di un periodo individualmente per qualcuno entity_id ? Ad ogni modo, si prega di fornire la definizione della tabella, la versione di Postgres, alcuni dati di esempio e il risultato previsto per il campione.
Erwin Brandstetter,

In teoria, intendevo ogni giorno, ma in pratica non è necessario considerare i giorni in cui non ci sono transazioni. Ho pubblicato i dati di esempio e la definizione della tabella.
tufelkinder,

Quindi vuoi accumulare righe dello stesso entity_idin una finestra di 30 giorni a partire da ogni transazione effettiva. Possono esserci più transazioni per la stessa (trans_date, entity_id)o tale combinazione è definita unica? La definizione della tabella non ha alcun UNIQUEvincolo o PK, ma i vincoli sembrano mancare ...
Erwin Brandstetter,

L'unico vincolo è sulla idchiave primaria. Possono esserci più transazioni per entità al giorno.
tufelkinder,

Informazioni sulla distribuzione dei dati: ci sono voci (per entity_id) per la maggior parte dei giorni?
Erwin Brandstetter,

Risposte:


26

La tua domanda

È possibile semplificare la query utilizzando una WINDOWclausola, ma ciò sta semplicemente accorciando la sintassi, non modificando il piano di query.

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);
  • Anche usando il leggermente più veloce count(*), dal momento che idè sicuramente definito NOT NULL?
  • E non è necessario ORDER BY entity_idpoiché giàPARTITION BY entity_id

Puoi semplificare ulteriormente, però:
non aggiungere affatto ORDER BYalla definizione della finestra, non è rilevante per la tua query. Quindi non è necessario definire una cornice della finestra personalizzata:

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

Più semplice, più veloce, ma comunque solo una versione migliore di ciò che hai , con mesi statici .

La query che potresti desiderare

... non è chiaramente definito, quindi mi baserò su questi presupposti:

Conta le transazioni e l'importo per ogni periodo di 30 giorni entro la prima e l'ultima transazione di qualsiasi entity_id. Escludere periodi iniziali e finali senza attività, ma includere tutti i possibili periodi di 30 giorni all'interno di tali limiti esterni.

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

Questo elenca tutti i periodi di 30 giorni per ciascuno entity_idcon i tuoi aggregati e con trans_dateil primo giorno (incl.) Del periodo. Per ottenere valori per ogni singola riga, unisciti nuovamente alla tabella di base ...

La difficoltà di base è la stessa discussa qui:

La definizione del frame di una finestra non può dipendere dai valori della riga corrente.

E piuttosto chiama generate_series()con timestampinput:

La query che desideri effettivamente

Dopo l'aggiornamento della domanda e la discussione:
accumula righe dello stesso entity_idin una finestra di 30 giorni a partire da ogni transazione effettiva.

Poiché i tuoi dati sono distribuiti in modo sparso, dovrebbe essere più efficiente eseguire un self-join con una condizione di intervallo , tanto più che Postgres 9.1 non ha ancora dei LATERALjoin:

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.

Una finestra mobile potrebbe avere senso (rispetto alle prestazioni) con i dati per quasi tutti i giorni.

Questo non aggrega duplicati (trans_date, entity_id)al giorno, ma tutte le righe dello stesso giorno sono sempre incluse nella finestra di 30 giorni.

Per un grande tavolo, un indice di copertura come questo potrebbe aiutare un po ':

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

L'ultima colonna amountè utile solo se si ottengono scansioni solo indice. Altrimenti rilasciarlo.

Ma non verrà utilizzato mentre selezioni comunque l'intero tavolo. Supporterebbe le query per un piccolo sottoinsieme.


Sembra davvero buono, testandolo ora sui dati e cercando di capire tutto ciò che la tua query sta effettivamente facendo ...
tufelkinder,

@tufelkinder: aggiunta una soluzione per la domanda aggiornata.
Erwin Brandstetter,

Esaminandolo ora. Sono incuriosito dal fatto che venga eseguito in SQL Fiddle ... Quando provo a eseguirlo direttamente sul mio transazionedb, si verifica un errore concolumn "t0.amount" must appear in the GROUP BY clause...
tufelkinder

@tufelkinder: ho ridotto il test case a 100 righe. sqlfiddle limita la dimensione dei dati di test. Jake (l'autore) ha ridotto i limiti un paio di mesi fa, quindi il sito è bloccato più facilmente.
Erwin Brandstetter,

1
Ci scusiamo per il ritardo, necessario per testarlo sul database completo. La tua risposta è stata superbamente approfondita ed educativa, come sempre. Grazie!
tufelkinder,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.