PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Tabelle der durchschnittlichen Aktienhistorie

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

SQL-Fiddle .

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