Специална трудност на тази задача:не можете просто да изберете точки от данни във вашия времеви диапазон, но трябва да вземете предвид най-новите точка от данни преди времевия диапазон и най-ранния точка от данни след времевият диапазон допълнително. Това варира за всеки ред и всяка точка от данни може да съществува или да не съществува. Изисква сложна заявка и затруднява използването на индекси.
Можете да използвате типове диапазони и оператори (Postgres 9.2+ ), за да опростите изчисленията:
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;
Имайте предвид, че използвам името на колоната day
вместо date
. Никога не използвам имена на основни типове като имена на колони.
В подзаявката sub
Извличам деня от следващия ред за всеки елемент с прозоречната функция lead()
, използвайки вградената опция за предоставяне на „днес“ по подразбиране, където няма следващ ред.
С това формирам daterange
и го съпоставете с входа с оператора за припокриване &&
, изчислявайки получения период от време с оператора за пресичане *
.
Всички гами тук са сизключително горна граница. Ето защо добавям един ден към диапазона на въвеждане. По този начин можем просто да извадим lower(range)
от upper(range)
за да получите броя на дните.
Предполагам, че "вчера" е последният ден с надеждни данни. „Днес“ все още може да се промени в реално приложение. Следователно използвам „днес“ (now()::date
) като изключителна горна граница за отворени диапазони.
Предоставям два резултата:
-
your_result
е съгласен с показаните от вас резултати.
Делите безусловно на броя дни във вашия период от време. Например, ако даден артикул е посочен само за последния ден, получавате много ниска (подвеждаща!) „средна стойност“. -
my_result
изчислява същите или по-големи числа.
Деля на действителното брой дни в списъка на даден артикул. Например, ако даден артикул е посочен само за последния ден, връщам посочената стойност като средна.
За да осмисля разликата, добавих броя дни, в които артикулът е бил в списъка:days_in_range
Индекс и ефективност
За този тип данни старите редове обикновено не се променят. Това би било отличен случай за материализиран изглед :
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;
След това можете да добавите GiST индекс, който поддържа съответния оператор &&
:
CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);
Голям тестов случай
Проведох по-реалистичен тест с 200k реда. Заявката, използваща MV, беше около 6 пъти по-бърза, което от своя страна беше ~ 10 пъти по-бърза от заявката на @Joop. Производителността силно зависи от разпространението на данни. MV помага най-много при големи маси и висока честота на влизания. Освен това, ако таблицата има колони, които не са подходящи за тази заявка, MV може да бъде по-малък. Въпрос на цена срещу печалба.
Сложих всички решения, публикувани досега (и адаптирани) в голяма цигулка, за да си играя с:
SQL Fiddle с голям тестов случай.
SQL Fiddle само с 40k реда
- за избягване на таймаут на sqlfiddle.com