Заметки о данных и инженерии

Оконные функции: когда 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 почти никогда не то, что вы имели в виду.

Что ещё пригодилось

← ко всем записям