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
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