La difficoltà speciale di questa attività:non puoi semplicemente selezionare punti dati all'interno del tuo intervallo di tempo, ma devi considerare gli più recenti punto dati prima l'intervallo di tempo e il prima punto dati dopo inoltre l'intervallo di tempo. Questo varia per ogni riga e ogni punto dati può esistere o meno. Richiede una query sofisticata e rende difficile l'utilizzo degli indici.
Potresti usare tipi di intervallo e operatori (Postgres 9.2+ ) per semplificare i calcoli:
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;
Nota, utilizzo il nome della colonna day
invece di date
. Non uso mai i nomi dei tipi di base come nomi di colonna.
Nella sottoquery sub
Prendo il giorno dalla riga successiva per ogni elemento con la funzione della finestra lead()
, utilizzando l'opzione incorporata per fornire "oggi" come predefinito dove non c'è una riga successiva.
Con questo formiamo un daterange
e confrontalo con l'input con l'operatore di sovrapposizione &&
, calcolando l'intervallo di date risultante con l'operatore di intersezione *
.
Tutte le gamme qui sono con esclusiva bordo superiore. Ecco perché aggiungo un giorno all'intervallo di input. In questo modo possiamo semplicemente sottrarre lower(range)
da upper(range)
per ottenere il numero di giorni.
Presumo che "ieri" sia l'ultimo giorno con dati affidabili. "Oggi" può ancora cambiare in un'applicazione di vita reale. Di conseguenza, uso "oggi" (now()::date
) come bordo superiore esclusivo per gli intervalli aperti.
Fornisco due risultati:
-
your_result
concorda con i risultati visualizzati.
Dividi incondizionatamente per il numero di giorni nell'intervallo di date. Ad esempio, se un articolo viene elencato solo per l'ultimo giorno, ottieni una "media" molto bassa (fuorviante!). -
my_result
calcola numeri uguali o superiori.
Divido per effettivo numero di giorni in cui un articolo è elencato. Ad esempio, se un articolo è elencato solo per l'ultimo giorno, restituisco il valore indicato come medio.
Per dare un senso alla differenza ho aggiunto il numero di giorni in cui l'articolo è stato elencato:days_in_range
Indice e performance
Per questo tipo di dati, le vecchie righe in genere non cambiano. Questo sarebbe un ottimo caso per una visione materializzata :
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;
Quindi puoi aggiungere un indice GiST che supporta l'operatore pertinente &&
:
CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);
Grande banco di prova
Ho eseguito un test più realistico con 200.000 righe. La query che utilizzava la MV era circa 6 volte più veloce, che a sua volta era ~ 10 volte più veloce della query di @Joop. Le prestazioni dipendono fortemente dalla distribuzione dei dati. Un MV aiuta di più con tavoli grandi e alta frequenza di voci. Inoltre, se la tabella contiene colonne non rilevanti per questa query, una MV può essere più piccola. Una questione di costo contro guadagno.
Ho messo tutte le soluzioni pubblicate finora (e adattate) in un grande violino con cui giocare:
SQL Fiddle con grande banco di prova.
SQL Fiddle con solo 40.000 righe
- per evitare il timeout su sqlfiddle.com