Оконные функции: когда GROUP BY уже мало
Классическая задача: показать каждую продажу и рядом — её долю от общего итога по региону.
С GROUP BY так не выйдет: он схлопывает строки, а нам нужна и детализация, и агрегат одновременно.
Первое, что приходит в голову — подзапрос:
SELECT s.id, s.region, s.amount,
s.amount / t.total AS share
FROM sales s
JOIN (SELECT region, SUM(amount) AS total
FROM sales GROUP BY region) t
ON t.region = s.region;
Работает, но громоздко. То же самое через окно — одна строка:
SELECT id, region, amount,
amount / SUM(amount) OVER (PARTITION BY region) AS share
FROM sales;
ROWS против RANGE — где ловушка
Вот здесь начинается интересное. При нарастающем итоге легко написать так и не заметить проблемы:
SUM(amount) OVER (ORDER BY dt) -- RANGE по умолчанию!
SUM(amount) OVER (ORDER BY dt ROWS UNBOUNDED PRECEDING) -- построчно
Разница вылезает, когда в ORDER BY есть дубликаты.
RANGE берёт все строки с тем же значением сортировки — то есть все продажи
за одну дату схлопнутся в одинаковый итог. ROWS честно считает построчно.
| Режим | Что попадает в окно | Когда уместно |
|---|---|---|
RANGE | все строки с равным значением ORDER BY | итоги по датам целиком |
ROWS | ровно N строк от текущей | нарастающий итог, скользящее среднее |
Коварство в том, что на тестовых данных с уникальными датами оба варианта дают одинаковый результат. Расхождение всплывает уже на проде, когда в один день приходит несколько записей.
Правило простое: если пишете нарастающий итог — указывайтеROWSявно. УмолчаниеRANGEпочти никогда не то, что вы имели в виду.
Что ещё пригодилось
LAG/LEAD— сравнить с предыдущим периодом без self-joinROW_NUMBER()сPARTITION BY— взять последнюю запись по каждому ключуFILTER (WHERE ...)— условная агрегация внутри окна, читается лучшеCASE