specjalna trudność tego zadania:nie możesz po prostu wybrać punktów danych w swoim zakresie czasowym, ale musisz wziąć pod uwagę najnowsze punkt danych przed zakres czasu i najwcześniejszy punkt danych za dodatkowo zakres czasu. Różni się to dla każdego wiersza, a każdy punkt danych może istnieć lub nie. Wymaga złożonego zapytania i utrudnia korzystanie z indeksów.
Możesz użyć typów zakresów i operatorzy (Postgres 9.2+ ) aby uprościć obliczenia:
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;
Uwaga, używam nazwy kolumny day
zamiast date
. Nigdy nie używam podstawowych nazw typów jako nazw kolumn.
W podzapytaniu sub
Pobieram dzień z następnego wiersza dla każdego elementu za pomocą funkcji okna lead()
, używając wbudowanej opcji, aby domyślnie podać „dzisiaj” tam, gdzie nie ma następnego wiersza.
Dzięki temu tworzę daterange
i dopasuj go do danych wejściowych za pomocą operatora nakładania się &&
, obliczając wynikowy zakres dat za pomocą operatora przecięcia *
.
Wszystkie zakresy są tutaj z wyłącznym górna granica. Dlatego dodaję jeden dzień do zakresu wejściowego. W ten sposób możemy po prostu odjąć lower(range)
od upper(range)
aby uzyskać liczbę dni.
Zakładam, że „wczoraj” to ostatni dzień z wiarygodnymi danymi. „Dzisiaj” wciąż może się zmienić w aplikacji z prawdziwego życia. W związku z tym używam "dzisiaj" (now()::date
) jako wyłączna górna granica dla otwartych zakresów.
Podaję dwa wyniki:
-
your_result
zgadza się z wyświetlanymi wynikami.
Bezwarunkowo dzielisz przez liczbę dni w swoim zakresie dat. Na przykład, jeśli przedmiot jest wystawiony tylko na ostatni dzień, otrzymujesz bardzo niską (mylącą!) „średnią”. -
my_result
oblicza te same lub wyższe liczby.
Dzielę przez rzeczywistą liczba dni, przez które przedmiot jest wystawiony na aukcję. Na przykład, jeśli przedmiot jest wystawiony tylko na ostatni dzień, zwracam wymienioną wartość jako średnią.
Aby zrozumieć różnicę, dodałem liczbę dni, przez które przedmiot był wystawiany:days_in_range
Indeks i wydajność
W przypadku tego rodzaju danych stare wiersze zazwyczaj się nie zmieniają. Byłoby to doskonałym argumentem za zmaterializowanym widokiem :
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;
Następnie możesz dodać indeks GiST, który obsługuje odpowiedni operator &&
:
CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);
Duży przypadek testowy
Przeprowadziłem bardziej realistyczny test z 200 tysiącami wierszy. Zapytanie używające MV było około 6 razy szybsze, co z kolei było ~10x szybsze niż zapytanie @Joop. Wydajność w dużym stopniu zależy od dystrybucji danych. MV pomaga najbardziej w przypadku dużych tabel i dużej częstotliwości wpisów. Ponadto, jeśli tabela zawiera kolumny, które nie są istotne dla tego zapytania, MV może być mniejsza. Kwestia kosztów a zysków.
Wszystkie rozwiązania opublikowane do tej pory (i zaadaptowane) umieściłem na wielkich skrzypcach do zabawy:
SQL Fiddle z dużym przypadkiem testowym.
SQL Fiddle z zaledwie 40 tys. wierszy
- aby uniknąć przekroczenia limitu czasu na sqlfiddle.com