sql >> Database teknologi >  >> RDS >> PostgreSQL

Gennemsnitlig lagerhistoriktabel

Den særlige sværhedsgrad af denne opgave:du kan ikke bare vælge datapunkter inden for dit tidsinterval, men skal overveje det nyeste datapunkt før tidsintervallet og det tidligste datapunkt efter tidsintervallet yderligere. Dette varierer for hver række, og hvert datapunkt eksisterer muligvis ikke. Kræver en sofistikeret forespørgsel og gør det svært at bruge indekser.

Du kan bruge områdetyper og operatører (Postgres 9.2+ ) for at forenkle beregninger:

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;

Bemærk, jeg bruger kolonnenavnet day i stedet for date . Jeg bruger aldrig grundlæggende typenavne som kolonnenavne.

I underforespørgslen sub Jeg henter dagen fra næste række for hvert element med vinduesfunktionen lead() , ved at bruge den indbyggede mulighed for at give "i dag" som standard, hvor der ikke er nogen næste række.
Med dette danner jeg et daterange og match det med input med overlapningsoperatoren && , der beregner det resulterende datointerval med skæringsoperatoren * .

Alle intervaller her er med eksklusiv øvre grænse. Det er derfor, jeg tilføjer en dag til inputområdet. På denne måde kan vi simpelthen trække lower(range) fra fra upper(range) for at få antallet af dage.

Jeg går ud fra, at "i går" er den seneste dag med pålidelige data. "I dag" kan stadig ændre sig i en applikation i det virkelige liv. Derfor bruger jeg "i dag" (now()::date ) som eksklusiv øvre grænse for åbne områder.

Jeg giver to resultater:

  • your_result stemmer overens med dine viste resultater.
    Du dividerer ubetinget med antallet af dage i dit datointerval. For eksempel, hvis en vare kun er opført for den sidste dag, får du et meget lavt (vildledende!) "gennemsnit".

  • my_result beregner de samme eller højere tal.
    Jeg dividerer med det faktiske antal dage en vare er opført. For eksempel, hvis en vare kun er opført for den sidste dag, returnerer jeg den anførte værdi som gennemsnit.

For at forstå forskellen tilføjede jeg antallet af dage, varen var opført:days_in_range

SQL violin .

Indeks og ydeevne

For denne type data ændres gamle rækker typisk ikke. Dette ville være en glimrende sag for en materialiseret visning :

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;

Så kan du tilføje et GiST-indeks, som understøtter den relevante operatør && :

CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);

Stor testcase

Jeg kørte en mere realistisk test med 200k rækker. Forespørgslen ved hjælp af MV var omkring 6 gange så hurtig, hvilket igen var ~ 10 gange så hurtigt som @Joops forespørgsel. Ydeevnen afhænger i høj grad af datafordelingen. En MV hjælper de fleste med store borde og høj frekvens af tilmeldinger. Hvis tabellen har kolonner, der ikke er relevante for denne forespørgsel, kan en MV også være mindre. Et spørgsmål om omkostninger vs. gevinst.

Jeg har lagt alle de hidtil opslåede løsninger (og tilpasset) i en stor violin at lege med:

SQL Fiddle med stor testcase.
SQL Fiddle med kun 40.000 rækker - for at undgå timeout på sqlfiddle.com



  1. Vælg ord mellem kommaer (undtagen dobbelte anførselstegn) ved hjælp af regexp_substr

  2. Forskellen mellem BYTE og CHAR i kolonnedatatyper

  3. OraOLEDB.Oracle-udbyderen er ikke registreret på den lokale maskine

  4. Mulige konsekvenser af at øge varchar-længden i MySql?