De speciale moeilijkheid van deze taak:u kunt niet zomaar datapunten binnen uw tijdbereik kiezen, maar moet rekening houden met de nieuwste datapunt voor het tijdbereik en de vroegste datapunt na bovendien het tijdsbestek. Dit varieert voor elke rij en elk gegevenspunt kan al dan niet bestaan. Vereist een geavanceerde zoekopdracht en maakt het moeilijk om indexen te gebruiken.
U kunt bereiktypen gebruiken en operators (Postgres 9.2+ ) om berekeningen te vereenvoudigen:
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;
Let op, ik gebruik de kolomnaam day
in plaats van date
. Ik gebruik nooit basistypenamen als kolomnamen.
In de subquery sub
Ik haal de dag uit de volgende rij voor elk item met de vensterfunctie lead()
, waarbij de ingebouwde optie wordt gebruikt om "vandaag" als standaard op te geven als er geen volgende rij is.
Hiermee vorm ik een daterange
en vergelijk het met de invoer met de overlap-operator &&
, berekent het resulterende datumbereik met de intersectie-operator *
.
Alle reeksen hier zijn met exclusief bovenrand. Daarom voeg ik een dag toe aan het invoerbereik. Op deze manier kunnen we eenvoudig lower(range)
. aftrekken van upper(range)
om het aantal dagen te krijgen.
Ik neem aan dat "gisteren" de laatste dag is met betrouwbare gegevens. "Vandaag" kan nog veranderen in een real-life applicatie. Daarom gebruik ik "vandaag" (now()::date
) als exclusieve bovenrand voor open bereiken.
Ik geef twee resultaten:
-
your_result
gaat akkoord met uw weergegeven resultaten.
U deelt onvoorwaardelijk door het aantal dagen in uw datumbereik. Als een item bijvoorbeeld alleen voor de laatste dag wordt vermeld, krijgt u een zeer laag (misleidend!) "gemiddelde". -
my_result
berekent dezelfde of hogere getallen.
Ik deel door de werkelijke aantal dagen dat een item wordt vermeld. Als een item bijvoorbeeld alleen voor de laatste dag wordt vermeld, retourneer ik de vermelde waarde als gemiddelde.
Om het verschil te begrijpen, heb ik het aantal dagen toegevoegd dat het item werd vermeld:days_in_range
Index en prestaties
Voor dit soort gegevens veranderen oude rijen meestal niet. Dit zou een uitstekende zaak zijn voor een gematerialiseerde weergave :
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;
Vervolgens kunt u een GiST-index toevoegen die de relevante operator
CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);
Grote testcase
Ik heb een meer realistische test uitgevoerd met 200k rijen. De query met behulp van de MV was ongeveer 6 keer zo snel, wat op zijn beurt ~ 10x zo snel was als de query van @Joop. Prestaties zijn sterk afhankelijk van gegevensdistributie. Een MV helpt het meest bij grote tafels en een hoge deelnamefrequentie. Als de tabel kolommen heeft die niet relevant zijn voor deze zoekopdracht, kan een MV ook kleiner zijn. Een kwestie van kosten versus winst.
Ik heb alle oplossingen die tot nu toe zijn gepost (en aangepast) in een grote viool gestopt om mee te spelen:
SQL Fiddle met grote testcase.
SQL Fiddle met slechts 40k rijen
- om time-out op sqlfiddle.com te voorkomen