Разбираем магию оконных функций (на примере PostgreSQL)

Всем привет! Рассмотрим очень полезный и невероятно интересный функционал реляционных БД – оконные функции.

Примеры работают в PostgreSQL, однако мы основное внимание уделим логике работы, которая заложена в сам принцип работы оконных функций и применяется в других SQL-диалектах – поэтому вы без труда сможете применять полученные знания практически в любой БД, делая поправку на синтаксис используемого диалекта. Также отметим — так как это вводная статья, мы решили ограничиться описанием базовых оконных функций, которые, вероятно, покроют 90% задач, в которых эти функции необходимы. Во второй статье углубимся в код, рассмотрим оконные функции с фреймами, а также познакомимся с другими оконными конструкциями, нередко помогающими в работе аналитику.

Разбираем магию оконных функций (на примере PostgreSQL)

На первый взгляд может показаться, что оконные функции — это как group by. Вот отличие – конструкция group by собирает агрегат таблицы (изменяет количество строк в результирующем наборе данных, группирует строки), а оконные функции не группируют строки, а добавляют новые атрибуты, результат которых рассчитывает оконная функция.

Для удобства изучения мы решили сначала визуально показать, что из себя представляют оконные функции, дальше немного углубимся в логику и код. Итак, посмотрите на эту таблицу:

Разбираем магию оконных функций (на примере PostgreSQL)

Здесь мы выделили атрибуты, относящиеся к источнику данных (первоначальной таблице, блок «Исходная таблица»), а также атрибуты, которые рассчитываются с помощью базовых оконных функций (блок «Оконные функции»). Мы умышленно в каждое последующее окно поместили на один элемент больше, чтобы можно было невооруженным взглядом понять суть оконной функции, то, как изменяется ее значение. Зависимости показали красными линиями – то есть на результат оконной функции sum() влияет только атрибут «PRICE», а оконные конструкции count() и row_number() используют количество строк (для примера мы сослались на атрибут «ID»).

Теперь стало понятнее? Отлично. Давайте разберем детально каждую из трех оконных функций.

Оконная конструкция SUM()

Сразу пишем код, потом разбираем, что делает каждый символ:

Select ID, PRODUCT, TYPE, PRICE, sum(PRICE) over (partition by TYPE) as func_sum from ORDERS

Результат:

Разбираем магию оконных функций (на примере PostgreSQL)

Обратите внимание, что после применения оконной функции количество строк не изменилось, однако в столбце «SUM()» появился агрегат. Поэтому внимательнее пишите код дальше, чтобы случайно не построить агрегат на атрибуте-результате оконной функции. То есть конструкция

Select TYPE, sum(PRICE), sum(func_sum) from ORDERS

скорее всего, ошибочна, так как здесь «агрегат-на-агрегате».

Возвращаемся к разбору функции SUM(). Конструкция over (partition by TYPE) в данном случае формирует окно/partition, в которое помещает типы устройств, и в них считает sum(PRICE), то есть цену всех устройств. Обратите внимание, что если в атрибуте TYPE у вас есть, например, значения «Ноутбук» и «Ноутбукъ», то корректного окна у вас не получится. Точнее у вас будут 2 окна с ноутбуками. Поэтому сначала рекомендуем посмотреть на группы товаров, например, с использованием такой проверки:

Select TYPE, count(*) from orders group by TYPE

Результат может неприятно вас удивить, и вместо:

Разбираем магию оконных функций (на примере PostgreSQL)

Вы получите

Разбираем магию оконных функций (на примере PostgreSQL)

С этим разобрались – идем дальше.

Оконная конструкция COUNT()

Функция count() в классическом понимании считает количество строк таблицы. В данном случае она считает количество строк в окне, или количество устройств (product, строк таблицы) в каждой группе (TYPE):

Select ID, PRODUCT, TYPE, PRICE, count(*) over (partition by TYPE) as func_count from ORDERS
Разбираем магию оконных функций (на примере PostgreSQL)

Оконная конструкция ROW_number()

Функция нумерует строки (устройств, product) в окне (TYPE). Однако, когда мы нумеруем строки в таблице — нам необходимо определить порядок подсчета, то есть какой продукт будет номером 1, какой – номером 2 и т.д. Как это выглядит в коде:

Select ID, PRODUCT, TYPE, PRICE, row_number() over (partition by TYPE order by PRODUCT asc) as func_row_number from ORDERS

Здесь мы добавили сортировку через классическую конструкцию order by, указав, какой атрибут будем сортировать (product), а также направление сортировки (asc — по возрастанию, или desc – по убыванию). Если не указывать направление сортировки – данные будут отсортированы по умолчанию – asc:

Разбираем магию оконных функций (на примере PostgreSQL)

На этом все – мы разобрали базовые оконные конструкции, которые считают агрегаты внутри групп (окон). Чтобы закрепить знания – предлагаем вам самостоятельно посчитать максимальную и минимальную стоимость (PRICE) устройства (product) в рамках групп устройств (TYPE) – это две оконные функции, которые мы здесь не рассматривали.

Во второй статье, как было сказано ранее, углубимся в вариативность построения окон. Спасибо за внимание и до встречи!

1313
10 комментариев

Много мелких неточностей и умолчаний, новичок, наткнувшись на них, скорее потеряет время впустую, чем разберется благодаря этому материалу. Запрос в примере "агрегат на агрегате" некорректен потому что в orders нет никакого func_sum ну и запрос без group by type не выполнится. Такой запрос скорее сведёт с ума в попытке понять от чего предостерегает автор.

3
Ответить

А где Вы нашли неточности? Мне как изучающему просто интересно - я ошибок не нашел. Как по мне - статья написана просто для новичка в оконных функциях. Просто дабы избежать большого кол-ва кода (создание доптаблиц, подзапросов) здесь это не показано. Имхо понятнее так, чем читать 3-4 страницы с лишним объяснением select from (select ...), create as select ну и тд. Статья мне зашла. Показывает ошибки, на которые я натыкался сам. В запросе func_sum нет group by, но здесь акцентировано внимание на ошибке в логике, а не на корректном запросе, в угоду компактности статьи. Запрос ошибочен, но смысл имхо другой. 3 основных "оконных" кода, о которых пишет автор - корректны. По крайней мере я как новичок написал бы именно так.

2
Ответить

Кажется кто-то в тиктоке увидел шутку про оконные функции и решил написать статью, вот только хабровским окном ошибся :)

1
Ответить

Комментарий недоступен

Ответить

Статьи для разработчиков публикуют не только на Хабр. Много кейсов для разработчиков вы можете найти в разделе "Разработка" https://vc.ru/dev

Ответить

Комментарий недоступен

Ответить

Если вы работаете с SQL и анализом данных, то можете столкнуться с похожей задачей. Описанный практический пример может сэкономить время на поиск и чтение дополнительных материалов.

1
Ответить