Die besondere Schwierigkeit dieser Aufgabe:Sie können nicht einfach Datenpunkte innerhalb Ihres Zeitbereichs auswählen, sondern müssen die neuesten berücksichtigen Datenpunkt vorher den Zeitraum und den frühesten Datenpunkt nach den Zeitbereich zusätzlich. Dies variiert für jede Zeile und jeder Datenpunkt kann vorhanden sein oder nicht. Erfordert eine ausgeklügelte Abfrage und erschwert die Verwendung von Indizes.
Sie könnten Bereichstypen verwenden und Operatoren (Postgres 9.2+ ), um Berechnungen zu vereinfachen:
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;
Beachten Sie, dass ich den Spaltennamen day
verwende statt date
. Ich verwende niemals grundlegende Typnamen als Spaltennamen.
In der Unterabfrage sub
Mit der Fensterfunktion lead()
hole ich mir für jedes Item den Tag aus der nächsten Zeile , indem ich die eingebaute Option verwende, um "heute" als Standard bereitzustellen, wo es keine nächste Zeile gibt.
Damit bilde ich einen daterange
und mit dem Überlappungsoperator &&
mit der Eingabe vergleichen , wobei der resultierende Datumsbereich mit dem Schnittpunktoperator *
berechnet wird .
Alle Sortimente hier sind mit exklusiv obere Grenze. Deshalb füge ich dem Eingabebereich einen Tag hinzu. Auf diese Weise können wir einfach lower(range)
subtrahieren von upper(range)
um die Anzahl der Tage zu erhalten.
Ich gehe davon aus, dass "gestern" der späteste Tag mit verlässlichen Daten ist. „Heute“ kann sich in einer realen Anwendung noch ändern. Folglich verwende ich „heute“ (now()::date
). ) als exklusive Obergrenze für offene Bereiche.
Ich liefere zwei Ergebnisse:
-
Ihr_Ergebnis
stimmt mit Ihren angezeigten Ergebnissen überein.
Sie dividieren bedingungslos durch die Anzahl der Tage in Ihrem Datumsbereich. Wenn ein Artikel beispielsweise nur für den letzten Tag aufgeführt ist, erhalten Sie einen sehr niedrigen (irreführenden!) "Durchschnitt". -
mein_ergebnis
berechnet die gleichen oder höhere Zahlen.
Ich teile durch tatsächlich Anzahl der Tage, an denen ein Artikel gelistet ist. Wenn ein Artikel beispielsweise nur für den letzten Tag gelistet ist, gebe ich den gelisteten Wert als Durchschnitt zurück.
Um den Unterschied zu verstehen, habe ich die Anzahl der Tage hinzugefügt, an denen der Artikel aufgelistet war:days_in_range
Index und Leistung
Bei dieser Art von Daten ändern sich alte Zeilen normalerweise nicht. Dies wäre ein hervorragendes Argument für eine materialisierte Ansicht :
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;
Dann können Sie einen GiST-Index hinzufügen, der den entsprechenden Operator
CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);
Großer Testfall
Ich habe einen realistischeren Test mit 200.000 Zeilen durchgeführt. Die Abfrage mit dem MV war etwa 6-mal so schnell, was wiederum ~ 10-mal so schnell war wie die Abfrage von @Joop. Die Leistung hängt stark von der Datenverteilung ab. Ein MV hilft am meisten bei großen Tabellen und einer hohen Häufigkeit von Einträgen. Auch wenn die Tabelle Spalten enthält, die für diese Abfrage nicht relevant sind, kann ein MV kleiner sein. Eine Frage des Kosten-Nutzen-Verhältnisses.
Ich habe alle bisher geposteten (und angepassten) Lösungen in eine große Geige gelegt, um damit zu spielen:
SQL Fiddle mit großem Testfall.
SQL Fiddle mit nur 40.000 Zeilen
- um eine Zeitüberschreitung auf sqlfiddle.com zu vermeiden