16 Занятие 16: Расширяемость / Агрегатные и оконные функции
Исходная страница в нативном Markdown: Расширяемость / Агрегатные и оконные функции
Примечание
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета.
HTML source:dev2_16_ext_aggregates.htmlArchive:DEV2-handouts-16.zipСтраниц PDF: 27
Page 1: Расширяемость / Агрегатные и оконные функции
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
В PostgreSQL имеется достаточно много встроенных агрегатных функций. Часть из них определена стандартом (такие, как min, max, sum, count и т. п.), часть является расширением стандарта.
Но иногда может оказаться полезным создать собственную агрегатную функцию. Такая функция работает очень просто.
Имеется некоторое состояние, представленное значением какого-либо типа данных, которое инициализируется определенным значением (например, тип numeric и начальное значение 0).
Для каждой строки агрегируемой выборки вызывается функция перехода, которая получает значение из текущей строки и должна обновить состояние (например, прибавляет текущее значение к состоянию).
В конце вызывается функция финализации, которая преобразует полученное в результате работы состояние в результат агрегатной функции (в нашем примере достаточно просто вернуть число — в итоге получается аналог функции sum).
Состояние должно включать сумму значений и их количество. Для его хранения создадим составной тип:
=>CREATETYPEaverage_stateAS(accumfloat,qtyfloat);
CREATETYPE
Теперь определим функцию перехода. Она возвращает новое состояние на основе текущего, прибавляя текущее значение к сумме и единицу к количеству.
Мы также включим в функцию отладочный вывод, чтобы иметь возможность наблюдать за ее вызовом.
Окно определяет рамку (агрегируемую выборку) для каждой строки
OVER ()
OVER (PARTITION BY)
1 2 3 4 5 1 2 3 4 5
1 2 3 4 5 1 2 3 4 5
1 2 3 4 5 1 2 3 4 5
1 2 3 4 5 1 2 3 4 5
1 2 3 4 5 1 2 3 4 5
Комментарии преподавателя
Оконные функции работают подобно агрегатным, вычисляя значение на основе некоторой выборки. Эта выборка называется рамкой. Но, в отличие от агрегатных функций, строки не сворачиваются в одну общую, а вместо этого значение вычисляется для каждой строки выборки.
Окно задается в предложении OVER после имени функции.
Если окно указано как OVER(), то оконная функция вычисляется на основе всех строк — одинаково для каждой строки выборки.
Если определение окна включает фразу PARTITION BY, то оконная функция вычисляется на основе групп строк (аналогично группировке GROUP BY). В этом случае для всех строк одной группы будет получено одинаковое значение функции.
Page 7: OVER()
Созданная нами агрегатная функция работает как оконная без всяких изменений:
Обратите внимание, что, поскольку рамка для всех строк одинакова, значение вычисляется только один раз, а не для для каждой строки.
И для предложения PARTITION BY:
Как только в определении окна мы указываем предложение ORDER BY, упорядочивающее строки выборки, предполагается, что рамка окна охватывает строки от самой первой до текущей. Это можно записать и явным образом: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
Например, если в качестве оконной функции используется sum, мы получаем сумму «нарастающим итогом».
Конечно, для каждой группы строк, определяемых предложением PARTITION BY, рамка будет располагаться в пределах этой группы.
Page 9: OVER(ORDER BY)
Если добавить к определению окна предложение ORDER BY, получим рамку, «хвост» которой стоит на месте, а голова движется вместе с текущей строкой:
Снова не понадобилось никаких изменений — все работает. Здесь видно, как каждая следующая строка последовательно добавляется к состоянию и вызывается функция финализации.
Полная форма того же запроса выглядит так:
SELECTn,average(n)OVER(ORDERBYn-- сортировка
ROWSBETWEENUNBOUNDEDPRECEDING-- от самого начала
ANDCURRENTROW-- до текущей строки
)FROMtest;
То же самое работает и в сочетании с PARTITION BY:
Если в предыдущих примерах рамка только расширялась (в нее добавлялись все новые значения), то теперь ранее добавленные значения значения могут «уходить» из рамки.
Чтобы пользовательская агрегатная функция работала эффективно в таком режиме, нужно реализовать функцию инверсии, которая устраняет значение из состояния.
Page 11: OVER(ROWS BETWEEN)
С помощью фразы ROWS BETWEEN можно задать любую необходимую конфигурацию рамки, указывая (в частности):
UNBOUNDED PRECEDING — с самого начала; n PRECEDING — n предыдущих; CURRENT ROW — текущая строка; n FOLLOWING — n следующих; UNBOUNDED FOLLOWING — до самого конца.
Рассмотрим вычисление «скользящего среднего» для трех значений. В отличие от предыдущих примеров из состояния должно «вычитаться» значение, уходящее из рамки, но у нас есть только функция «добавления». Единственный способ выполнить запрос — пересчитывать всю рамку заново:
=>CREATEAGGREGATEaverage(float)(-- обычный агрегат
stype=average_state,initcond='(0,0)',sfunc=average_transition,finalfunc=average_final,-- вариант с обратной функцией
mstype=average_state,minitcond='(0,0)',msfunc=average_transition,minvfunc=average_inverse,mfinalfunc=average_final);
Агрегатные функции могут выполняться в параллельном режиме. Основной процесс, выполняющий запрос, может создать несколько фоновых рабочих процессов, каждый из которых будет выполнять параллельную обработку части данных в узле плана. Затем полученные результаты передаются в основной процесс, который собирает их и формирует общий результат.
Чтобы пользовательские агрегатные функции могли работать параллельно, нужно реализовать функцию объединения двух состояний в одно общее.
Page 14: Параллелизм
Таблица с пятью строчками, конечно, слишком мала для параллельного выполнения. Возьмем больше данных:
Пересоздадим агрегат, указав новую функцию и подтвердив безопасность параллельного выполнения:
Page 15: Page 15
=>DROPAGGREGATEaverage(float);
DROPAGGREGATE
=>CREATEAGGREGATEaverage(float)(-- обычный агрегат
stype=average_state,initcond='(0,0)',sfunc=average_transition,finalfunc=average_final,combinefunc=average_combine,parallel=safe,-- вариант с обратной функцией
mstype=average_state,minitcond='(0,0)',msfunc=average_transition,minvfunc=average_inverse,mfinalfunc=average_final);
Здесь видно, что три процесса поделили работу примерно поровну, и затем три состояния были попарно объединены.
В оконном режиме параллельное выполнение не поддерживается, в том числе и для встроенных функций.
Page 16: Итоги
PostgreSQL позволяет создавать агрегатные и оконные функции
Агрегатные и оконные функции дают возможность использовать процедурную обработку в стиле SQL
Page 17: Практика
Напишите отчет, выводящий складские остатки по каждой книге в денежном выражении (используя закупочную, а не розничную цену). Оформите отчет как фоновое задание. Учтите, что поступления происходят разными партиями по разной цене, а продажи никак не привязаны к партиям. Поэтому считайте, что в первую очередь продаются книги из более старых партий. 2. Дополните расширение bookfmt функциями min и max для формата издания. Обновите версию расширения и убедитесь, что функции появились в базе данных.
Комментарии преподавателя
Начните с запроса к таблице operations, выводящего только поступления книг, но добавьте столбец, показывающий общее количество проданных книг данного поступления.
В качестве примера книги, для которой на складе остались экземпляры из нескольких партий, можно взять книгу с book_id = 15.
Напишите оконную функцию, вычисляющую при использовании в режиме «нарастающего итога» остаток книг данного поступления на складе (или решите задачу, используя имеющиеся оконные функции).
Расширение bookfmt было написано в практике к теме «Создание расширений».
Page 18: 1. Отчет по складским остаткам
Начнем с простого запроса, который выводит поступления книг и добавляет к каждой строке количество проданных экземпляров данной книги:
=>WITHsold(book_id,qty)AS(-- продажи книг
SELECTbook_id,-sum(qty)FROMoperationsWHEREqty<0GROUPBYbook_id)SELECTo.book_id,o.qty,s.qtyassold_qty,o.priceFROMoperationsoLEFTJOINsoldsONs.book_id=o.book_idWHEREo.qty>0ANDo.book_id=15-- для примера
ORDERBYo.book_id,o.at;
Нам хотелось бы иметь функцию, которая на основании столбцов qty и sold_qty вычислит остаток книг от каждого поступления.
Состояние такой агрегатной функции, которую мы будем использовать в режиме «нарастающего итога», включает два целых числа:
количество книг, уже распределенных между поступлениями; количество нераспроданных книг в текущем поступлении.
Функция перехода увеличивает количество распределенных книг на число книг в поступлении, но только до тех пор, пока оно не превышает общего числа проданных книг:
Добавим новую агрегатную функцию к запросу, указав группировку по книгам (book_id) и сортировку в порядке времени совершения операций:
=>WITHsold(book_id,qty)AS(-- продажи книг
SELECTbook_id,-sum(qty)::integerFROMoperationsWHEREqty<0GROUPBYbook_id)SELECTo.book_id,o.qty,s.qtyassold_qty,distribute(o.qty,s.qty)OVER(PARTITIONBYo.book_idORDERBYo.at)left_in_stock,o.priceFROMoperationsoLEFTJOINsoldsONs.book_id=o.book_idWHEREo.qty>0ANDo.book_id=15-- для примера
ORDERBYo.book_id,o.at;
Осталось убрать условие для конкретной книги, умножить остаток на цену, сгруппировать результат по книгам и оформить запрос в виде функции, чтобы зарегистрировать ее как фоновое задание:
Page 20: Page 20
=>CREATEFUNCTIONstock_task(paramsjsonbDEFAULTNULL)RETURNSTABLE(book_idbigint,costnumeric)LANGUAGEsqlSTABLEBEGINATOMICWITHsold(book_id,qty)AS(-- продажи книг
SELECTbook_id,-sum(qty)::integerFROMoperationsWHEREqty<0GROUPBYbook_id),left_in_stock(book_id,cost)AS(SELECTo.book_id,
Заметим, что задачу можно решить и с помощью стандартных оконных функций:
Page 21: Page 21
=>WITHsold(book_id,qty)AS(-- продажи книг
SELECTbook_id,-sum(qty)FROMoperationsWHEREqty<0GROUPBYbook_id),received(book_id,qty,cum_qty,price)AS(-- поступления книг
SELECTbook_id,qty,sum(qty)OVER(PARTITIONBYbook_idORDERBYat),priceFROMoperationsWHEREqty>0),left_in_stock(book_id,qty,price)AS(-- оставшиеся на складе книги
SELECTr.book_id,CASEWHENr.cum_qty-s.qty<0THEN0WHENr.cum_qty-s.qty<r.qtyTHENr.cum_qty-s.qtyELSEr.qtyEND,r.priceFROMreceivedrLEFTJOINsoldsONs.book_id=r.book_id)SELECTbook_id,sum(qty*price)FROMleft_in_stockGROUPBYbook_id
Создадим версию 1.1 расширения с учетом следующего:
Состоянием для агрегатной функции является текущее минимальное (максимальное) значение. Функция перехода записывает в состояние минимальное (максимальное) из двух значений — запомненного в состоянии и текущего. Функция финализации не нужна — возвращается просто текущее состояние.
При создании агрегата мы дополнительно указали оператор сортировки, реализующий стратегию «меньше» («больше») класса операторов, чтобы планировщик мог оптимизировать вызов наших агрегатных функций, просто получая первое значение из индекса.
bookfmt/Makefile
EXTENSION = bookfmt
DATA = bookfmt--0.sql bookfmt--0--1.0.sql bookfmt--1.0--1.1.sql
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
Напишите агрегатную функцию, вычисляющую средневзвешенное значение для совокупности элементов нескольких типов. Веса для типов элементов передаются функции в виде массива. 2. Бизнес-центр сдает офисы компаниям. В конце месяца администрации БЦ приходит общий счет за электроэнергию, который надо распределить между арендаторами пропорционально площади занимаемых помещений. Выставляемые арендаторам счета необходимо округлить до копеек, но так, чтобы их сумма совпала со значением, указанным в общем счете.
Комментарии преподавателя
Средневзвешенное значение совокупности элементов — среднее значение, учитывающее вес, или важность, каждого элемента. Значение вычисляется по формуле (x1w(t1) + x2w(t2) + … + xnw(tn)) / (w(t1) + w(t2) + … + w(tn)), где
xi — значение элемента, ti — тип элемента, w(ti) — вес элемента данного типа.
Требуется написать агрегатную функцию следующего вида: w_agv(x float, t integer, w float[]).
Помещения бизнес-центра можно представить таблицей:
rent (
renter text PRIMARY KEY, -- арендатор
area integer -- площадь, м^2
);
Пример, показывающий проблему с обычным округлением:
You are now connected to database "ext_aggregates" as user "student".
Рассмотрим задачу на примере данных об оценках за экзаменационные работы. Пусть в экзаменационный билет входят теоретические вопросы (тип 1) и практические задания (тип 2), причем задания имеют больший вес в итоговой оценке.
А теперь подсчитаем средневзвешенные оценки за экзамен, сданный студентами, полагая, что решение практической задачи вдвое ценнее ответа на теоретический вопрос:
Функция перехода добавляет к состоянию округленную сумму и ошибку округления. А если ошибка округления переваливает за полкопейки, то добавляет копейку к сумме.
Пробуем. Нам нужен какой-то определенный, но не важно какой именно, порядок просмотра строк. В данном случае подходит арендатор, поскольку он уникален.