sql >> Databasteknik >  >> RDS >> PostgreSQL

Tabell för genomsnittlig aktiehistorik

Den särskilda svårigheten för denna uppgift:du kan inte bara välja datapunkter inom ditt tidsintervall, utan måste överväga det senaste datapunkt före tidsintervallet och det tidigaste datapunkt efter tidsintervallet dessutom. Detta varierar för varje rad och varje datapunkt kan existera eller inte. Kräver en sofistikerad fråga och gör det svårt att använda index.

Du kan använda intervalltyper och operatörer (Postgres 9.2+ ) för att förenkla beräkningar:

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;

Obs, jag använder kolumnnamnet day istället för date . Jag använder aldrig grundläggande typnamn som kolumnnamn.

I underfrågan sub Jag hämtar dagen från nästa rad för varje objekt med fönsterfunktionen lead() , med det inbyggda alternativet för att tillhandahålla "idag" som standard där det inte finns någon nästa rad.
Med detta bildar jag ett daterange och matcha den mot indata med överlappningsoperatorn && , beräknar det resulterande datumintervallet med korsningsoperatorn * .

Alla intervall här är med exklusiv övre gränsen. Det är därför jag lägger till en dag till inmatningsintervallet. På så sätt kan vi helt enkelt subtrahera lower(range) från upper(range) för att få antalet dagar.

Jag antar att "igår" är den senaste dagen med tillförlitliga data. "Idag" kan fortfarande förändras i en verklig applikation. Följaktligen använder jag "idag" (now()::date ) som exklusiv övre kant för öppna områden.

Jag ger två resultat:

  • your_result överensstämmer med dina visade resultat.
    Du dividerar ovillkorligen med antalet dagar i ditt datumintervall. Till exempel, om en vara endast listas för den sista dagen, får du ett mycket lågt (vilseledande!) "genomsnitt".

  • my_result beräknar samma eller högre tal.
    Jag dividerar med det faktiska antal dagar ett objekt är listat. Till exempel, om en vara bara listas för den sista dagen, returnerar jag det angivna värdet som genomsnitt.

För att förstå skillnaden lade jag till antalet dagar som artikeln var listad:days_in_range

SQL Fiddle .

Index och prestanda

För den här typen av data ändras vanligtvis inte gamla rader. Detta skulle vara ett utmärkt fall för en materialiserad vy :

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;

Sedan kan du lägga till ett GiST-index som stöder den relevanta operatören && :

CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);

Stort testfall

Jag körde ett mer realistiskt test med 200k rader. Frågan med hjälp av MV var ungefär 6 gånger så snabb, vilket i sin tur var ~ 10 gånger så snabb som @Joops fråga. Prestanda beror mycket på datadistribution. En MV hjälper de flesta med stora bord och hög frekvens av anmälningar. Dessutom, om tabellen har kolumner som inte är relevanta för den här frågan, kan en MV vara mindre. En fråga om kostnad kontra vinst.

Jag har lagt alla lösningar som lagts upp hittills (och anpassade) i en stor fiol att leka med:

SQL-fiol med stort testfall.
SQL-fiol med endast 40 000 rader - för att undvika timeout på sqlfiddle.com



  1. hur man använder funktionen require_once inside

  2. Finns det något sätt att spola ut utdata från PL/SQL i Oracle?

  3. Hur förhindrar man Mysql Connector/J från att konvertera tidszonen DATUM och TID?

  4. MySQL-uppdateringsfält baserat på lägsta värde för ett annat fält när det grupperas av ett tredje