17 Занятие 17: PL/pgSQL / Триггеры
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_17_plpgsql_triggers.html Archive: DEV1-handouts-16.zip Страниц PDF: 32
Page 1: PL/pgSQL / Триггеры
Авторские права
© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Триггеры и триггерные функции
- В какой момент срабатывают триггеры
- Контекст выполнения триггерной функции
- Возвращаемое значение
- Для чего можно применять триггеры и для чего не нужно
- Событийные триггеры
Page 3: Триггеры и функции
Триггер
- объект базы данных — список обрабатываемых событий
- при возникновении события вызывается триггерная функция
- и ей передается контекст
- Триггерная функция
- объект базы данных — код обработки события
- выполняется в той же транзакции, что и основная операция
- соглашение: функция не принимает параметры,
- возвращает значение псевдотипа trigger (фактически record)
- может использоваться в нескольких триггерах
Комментарии преподавателя
Механизм триггеров позволяет выполнять определенные действия «в ответ» на определенные события. Триггер состоит из двух частей: собственно триггера (который определяет события) и триггерной функции (которая определяет действия). И триггер, и функция являются самостоятельными объектами БД.
Когда возникает событие, на которое «подписан» триггер, вызывается триггерная функция. Ей передается контекст вызова, чтобы можно было определить, какой именно триггер и в каких условиях вызвал функцию.
Триггерная функция — это обычная функция, которая написана с учетом некоторых соглашений:
● она пишется на любом языке, кроме чистого SQL;
● она не имеет параметров;
● она возвращает значение типа trigger (на самом деле это псевдотип, по факту возвращается запись, соответствующая строке таблицы).
Триггерная функция выполняется в той же транзакции, что и основная операция. Таким образом, если триггерная функция завершится с ошибкой, вся транзакция будет прервана.
https://postgrespro.ru/docs/postgresql/16/trigger-definition
Page 4: События
INSERT, UPDATE, DELETE
- таблицы
- before/after statement
- before/after row
- представления
- before/after statement
- instead of
- row
- TRUNCATE
- таблицы
- before/after statement
- Условие WHEN
- устанавливает дополнительный фильтр
Комментарии преподавателя
Триггеры могут срабатывать на вставку (INSERT), обновление (UPDATE) или удаление (DELETE) строк в таблице или представлении, а также на опустошение (TRUNCATE) таблиц.
Триггер может срабатывать до выполнения действия (BEFORE), после него (AFTER), или вместо него (INSTEAD OF).
Триггер может срабатывать один раз для всей операции (FOR EACH STATEMENT), или каждый раз для каждой затронутой строки (FOR EACH ROW).
Не для любой комбинации этих условий можно создать триггер. Например, instead-of-триггеры можно определить только для представлений на уровне строк, а truncate-триггер можно определить только для таблиц и только на уровне оператора. Допустимые варианты перечислены на слайде.
Кроме того, можно сузить область действия триггера, указав дополнительное условие WHEN: если условие не выполняется — триггер не срабатывает.
https://postgrespro.ru/docs/postgresql/16/sql-createtrigger
Page 5: Before statement
Срабатывает
- BEFORE STATEMENT
- до операции
- выполнение операции
- Возвращаемое значение
- игнорируется
- Контекст
- TG-переменные
Комментарии преподавателя
Рассмотрим подробнее разные типы триггеров.
Триггер BEFORE STATEMENT срабатывает один раз для операции независимо от того, сколько строк будет затронуто (возможно, что и ни одной). Это происходит до того, как операция начала выполняться.
Возвращаемое значение триггерной функции игнорируется. Если в триггере возникает ошибка, операция отменяется.
Поскольку триггерная функция не имеет параметров, контекст вызова в PL/pgSQL передается ей с помощью предопределенных TG-переменных, таких, как:
● TG_WHEN = «BEFORE»,
● TG_LEVEL = «STATEMENT»,
● TG_OP = «INSERT»/«UPDATE»/«DELETE»/«TRUNCATE»
и др. Триггерной функции можно также передать пользовательский контекст (аналог отсутствующих параметров) через переменную TG_ARGV, хотя зачастую лучше вместо одной «обобщенной» функции создать несколько «частных».
https://postgrespro.ru/docs/postgresql/16/plpgsql-trigger
Page 6: Before row
Срабатывает
- BEFORE STATEMENT
- перед действием со строкой
- в процессе выполнения операции
- выполнение операции
- Возвращаемое значение
- BEFORE ROW
- строка (возможно, измененная)
- null отменяет действие
- Контекст
- OLD
- update, delete
- NEW
- insert, update
- TG-переменные
Комментарии преподавателя
Триггеры BEFORE ROW срабатывают каждый раз перед тем, как операция затронет строку. Это происходит непосредственно в процессе выполнения операции.
В качестве контекста триггерная функция получает переменные:
● OLD — старая строка (не определено для операции вставки),
● NEW — измененная строка (не определено для удаления),
● TG_WHEN = «BEFORE»,
● TG_LEVEL = «ROW»,
● TG_OP = «INSERT»/«UPDATE»/«DELETE»
и др.
Возврат неопределенного значения NULL воспринимается как отмена действия над данной строкой. Сама операция продолжит выполнение, но текущая строка не будет обработана и другие триггеры для этой строки не сработают.
Чтобы не вмешиваться в работу операции, триггер должен вернуть строку в том виде, в котором ее собирается изменить операция: NEW для вставки и обновления, любое значение (но не NULL) для удаления (обычно используют OLD).
Но триггерная функция может и изменить значение NEW, чтобы повлиять на результат операции — часто именно для этого такой триггер и создают.
Page 7: Instead of row
Срабатывает
- BEFORE STATEMENT
- вместо действия со строкой
- для представлений
- выполнение операции
- Возвращаемое значение
- BEFORE ROW
- INSTEAD OF ROW
- строка (возможно, измененная) —
- будет видна в RETURNING
- null отменяет действие
- Контекст
- OLD
- update, delete
- NEW
- insert, update
- TG-переменные
Комментарии преподавателя
Триггеры INSTEAD OF очень похожи на триггеры BEFORE, но определяются только для представлений и срабатывают не до, а вместо операции.
В задачу таких триггеров обычно входит выполнение необходимых операций над базовыми таблицами представления. Также триггер может вернуть измененное значение NEW — именно оно будет видно при выполнении операции с указанием фразы RETURNING.
Page 8: After row
Срабатывает
- BEFORE STATEMENT
- после выполнения операции
- очередь из прошедших условие WHEN
- выполнение операции
- Возвращаемое значение
- BEFORE ROW
- INSTEAD OF ROW
- игнорируется
- Контекст
- OLD, OLD TABLE
- update, delete
- NEW, NEW TABLE
- insert, update
- TG-переменные
- AFTER ROW
Комментарии преподавателя
Триггеры AFTER ROW, как и BEFORE ROW, срабатывают для каждой затрагиваемой строки, но не сразу после действия над строкой, а после того, как выполнена вся операция - чтобы при обращении из этих триггеров к изменяющейся таблице результат не зависел от порядка обработки строк. Для этого события сначала помещаются в очередь и обрабатываются после окончания операции. Чем меньше событий попадет в очередь, тем меньше будет накладных расходов — поэтому именно в этом случае очень полезно использовать условие WHEN, чтобы отсечь заведомо ненужные строки.
Возвращаемое значение триггеров AFTER ROW игнорируется (поскольку операция уже выполнена).
Контекст триггерной функции составляют:
OLD — старая строка (не определено для операции вставки),
NEW — новое значение строки (не определено для удаления).
Кроме этих переменных, начиная с версии 10 триггерная функция может получить доступ к специальным переходным таблицам (transition tables). Таблица, указанная при создании триггера как OLD TABLE, содержит старые значения строк, обработанных триггером, а таблица NEW TABLE — новые значения тех же строк.
Доступны и обычные TG-переменные, включая:
● TG_WHEN = «AFTER»,
● TG_LEVEL = «ROW»,
● TG_OP = «INSERT»/«UPDATE»/«DELETE».
Page 9: After statement
Срабатывает
- BEFORE STATEMENT
- после операции
- (даже если не затронута ни одна строка)
- выполнение операции
- Возвращаемое значение
- BEFORE ROW
- INSTEAD OF ROW
- игнорируется
- Контекст
- OLD TABLE
- update, delete
- NEW TABLE
- insert, update
- TG-переменные
- AFTER ROW
- AFTER STATEMENT
Комментарии преподавателя
Триггер AFTER STATEMENT срабатывает один раз после окончания операции и после всех триггеров AFTER ROW (независимо от того, сколько строк было затронуто).
Возвращаемое значение триггерной функции игнорируется.
Контекст вызова передается с помощью переходных таблиц. Обращаясь к ним, триггерная функция может проанализировать все затронутые строки. Обычно переходные таблицы используются именно с триггерами AFTER STATEMENT, а не AFTER ROW.
Также определены обычные TG-переменные:
● TG_WHEN = «AFTER»,
● TG_LEVEL = «STATEMENT»,
● TG_OP = «INSERT»/«UPDATE»/«DELETE»/«TRUNCATE»
и др.
Page 10: Порядок вызова триггеров
Создадим «универсальную» триггерную функцию, которая описывает контекст, в котором она вызвана. Контекст передается в различных TG-переменных. Затем создадим триггеры на различные события, и будем смотреть, какие триггеры и в каком порядке вызываются при выполнении операций.
Таблица:
Триггеры на уровне оператора.
И на уровне строк:
Пробуем вставку:
Page 11: Page 11
Обновление:
Триггеры на уровне оператора сработают, даже если команда не обработала ни одной строки:
Тонкий момент: оператор INSERT с предложением ON CONFLICT приводит к тому, что срабатывают BEFORE-триггеры и на вставку, и на обновление:
И, наконец, удаление:
Для появившегося в PostgreSQL 15 оператора MERGE специального триггера нет, работают триггеры на обновление, удаление и вставку:
Переходные таблицы
Напишем триггерную функцию, показывающую содержимое переходных таблиц. Здесь мы используем имена old_table и new_table, которые будут объявлены при создании триггера.
Page 12: Page 12
Переходные таблицы «выглядят» настоящими, но не присутствуют в системном каталоге и располагаются в оперативной памяти (хотя и могут сбрасываться на диск при большом объеме).
Создадим новую таблицу:
Чтобы при выполнении операции создавались переходные таблицы, необходимо указывать их имена при создании триггера:
Проверим:
Переходные таблицы содержат только те строки, которые были затронуты операцией. Для операций вставки и удаления переходные таблицы работают точно так же, но доступна будет только одна из таблиц: NEW TABLE или OLD TABLE соответственно. Поскольку триггеры AFTER ROW срабатывают после выполнения всей операции, переходные таблицы можно использовать и в них. Но обычно это не имеет смысла.
Page 13: Возможное использование
- проверка применимости операции
- BEFORE STATEMENT
- выполнение операции
- проверка корректности;
- модификация строки
- BEFORE ROW
- изменение базовых таблиц представления
- INSTEAD OF ROW
- проверка согласованности,
- в том числе на уровне таблицы;
- «аудит» операций;
- каскадное изменение таблиц
- (денормализация, асинхронная
- обработка…)
- AFTER ROW
- AFTER STATEMENT
Комментарии преподавателя
Каково практическое применение триггеров?
BEFORE-триггеры можно использовать для проверки корректности операции и при необходимости вызывать ошибку.
Триггеры BEFORE ROW можно применять для модификации строки (например, заполнить пустое поле нужным значением). Это бывает удобно, чтобы не повторять логику заполнения «технических» полей в каждой операции, а также позволяет вмешаться в работу приложения, код которого недоступен для изменения.
Триггеры INSTEAD OF ROW применяют для того, чтобы отобразить операции над представлением в операции над базовыми таблицами.
Триггеры AFTER ROW и AFTER STATEMENT полезны в случаях, когда нужно знать точное состояние после операции (BEFORE-триггеры могут влиять на результат, так что на этом этапе ясности еще нет):
● для проверки согласованности операции;
● для «аудита», то есть записи изменений в отдельное хранилище;
● для каскадного изменения других таблиц (например, обновлять денормализованные данные при изменении базовых таблиц, или записывать изменения в очередь для последующей обработки вне данной транзакции).
Если операции затрагивают сразу много строк, то триггер AFTER STATEMENT с переходными таблицами может оказаться более эффективным решением, чем триггер AFTER ROW, поскольку позволяет обрабатывать все изменения пакетно, а не построчно.
Page 14: Сложности
Код вызывается неявно
- сложно отследить логику выполнения
- Правила видимости изменчивой триггерной функции
- виден результат триггеров BEFORE ROW или INSTEAD OF ROW
- Порядок вызова триггеров для одного события
- триггеры отрабатывают в алфавитном порядке
- Не предотвращается зацикливание
- триггер может вызвать срабатывание других триггеров
- Можно нарушить ограничения целостности
- например, исключив из обработки строки, которые должны удалиться
Комментарии преподавателя
Однако не стоит злоупотреблять триггерами. Триггеры срабатывают неявно, что сильно затрудняет понимание логики работы и крайне усложняет поддержку приложения. Попытки реализовать сложную логику на триггерах обычно заканчиваются плачевно.
В некоторых случаях вместо триггеров можно использовать вычисляемые поля (GENERATED ALWAYS AS … STORED). Такое решение — если оно подходит — будет заведомо проще и прозрачнее.
Есть ряд тонкостей, связанных с триггерами, которые мы сознательно не рассматриваем подробно:
● правила видимости изменчивых (volatile) функций в триггерах BEFORE ROW и INSTEAD OF ROW (не стоит обращаться к таблице, полагаясь на порядок, в котором сработают триггеры);
● порядок вызова нескольких триггеров, обрабатывающих одно и то же событие (не стоит усугублять и без того неявное срабатывание триггеров завязкой на последовательность обработки);
● возможность зацикливания в случае каскадного срабатывания других триггеров, которые, в свою очередь, могут приводить к новым срабатываниям данного триггера;
● возможность нарушить ограничения целостности (например, при исключении из обработки строки, которая удаляется условием ON DELETE CASCADE, может быть нарушена ссылочная целостность).
Если вы столкнулись с тем, что эти тонкости важны для вашего приложения — серьезно задумайтесь.
Page 15: Примеры использования триггеров
Пример 1: сохранение истории изменения строк. Пусть есть таблица, содержащая актуальные данные. Задача состоит в том, чтобы в отдельной таблице сохранять всю историю изменения строк основной таблицы. Поддержку исторической таблицы можно было бы возложить на приложение, но тогда велика вероятность, что в каких-то случаях из-за ошибок история не будет сохраняться. Поэтому решим задачу с помощью триггера. Основная таблица:
Историческая таблица должна называться так же, как основная, но заканчиваться на «_history». Сначала создаем клон основной таблицы…
…и затем добавляем столбцы «действительно с» и «действительно по»:
Одна триггерная функция будет вставлять новую историческую строку с открытым интервалом действия:
Другая функция будет закрывать интервал действия исторической строки:
Теперь создадим триггеры. Важные моменты: Обновление трактуется как удаление и последующая вставка; здесь важен порядок, в котором сработают триггеры (по алфавиту). Current_timestamp возвращает время начала транзакции, поэтому при обновлении start_date одной строки будет равен end_date другой. Использование AFTER-триггеров позволяет избежать проблем с INSERT … ON CONFLICT и потенциальными конфликтами с другими триггерами, которые могут существовать на основной таблице.
Page 16: Page 16
Проверим работу триггеров.
В исторической таблице хранится вся история изменений:
И теперь по ней можно восстановить состояние на любой момент времени (это немного напоминает работу механизма MVCC). Например, на самое начало:
Примеры использования триггеров
Пример 2: обновляемое представление. Пусть имеются две таблицы: аэропорты и рейсы:
Page 17: Page 17
Для удобства можно определить представление:
Но такое представление не допускает изменений. Например, не получится изменить пункт назначения таким образом:
Однако мы можем определить триггер. Триггерная функция может выглядеть, например, так (для краткости обрабатываем только аэропорт назначения, но не составит труда добавить и аэропорт вылета):
И сам триггер:
Проверим:
Page 18: Page 18
Попытка изменить аэропорт на отсутствующий в таблице:
Page 19: Событийные триггеры
Событийный триггер
- похож на обычный «табличный» триггер, но другой объект
- Триггерная функция
- соглашение: функция не принимает параметры,
- возвращает значение псевдотипа event_trigger
- для получения контекста служат специальные функции
- События
- DDL_COMMAND_START
- перед выполнением команды
- DDL_COMMAND_END
- после выполнения команды
- TABLE_REWRITE
- перед перезаписью таблицы
- SQL_DROP
- после удаления объектов
Комментарии преподавателя
Событийные триггеры — по сути те же триггеры, но срабатывают они не на DML-, а на DDL-операции (CREATE, ALTER, DROP, COMMENT, GRANT, REVOKE) и не привязаны к определенному объекту базы данных.
Такие триггеры являются не инструментом разработки приложений, а скорее служат для решения задач администрирования. Поэтому здесь мы упоминаем их только для полноты картины и рассмотрим только простые примеры.
https://postgrespro.ru/docs/postgresql/16/event-trigger-definition
https://postgrespro.ru/docs/postgresql/16/event-trigger-matrix
https://postgrespro.ru/docs/postgresql/16/sql-createeventtrigger
https://postgrespro.ru/docs/postgresql/16/functions-event-triggers
Page 20: Триггеры событий
Пример триггера для события ddl_command_end — завершение DDL-операции. Создадим функцию, которая описывает контекст вызова:
Сам триггер:
Создаем новую таблицу:
Создание таблицы может привести к выполнению нескольких команд DDL, поэтому функция pg_event_trigger_ddl_commands возвращает множество строк. Триггер на событие table_rewrite срабатывает до перезаписи таблицы командами ALTER TABLE и ALTER TYPE. Функция ниже выведет информацию о перезаписываемой таблице и код, описывающий причину перезаписи:
Теперь создаем триггер:
Изменим таблицу — назначим другой тип столбцу id:
Хотя перезапись таблицы может быть вызвана и другими командами, в частности CLUSTER и VACUUM FULL, событие table_rewrite для них не вызывается.
Page 21: Page 21
Page 22: Итоги
Триггер — способ отреагировать на возникновение события
- С помощью триггера можно отменить операцию, изменить ее результат или выполнить дополнительные действия
- Триггер выполняется как часть транзакции; ошибка в триггере приводит к откату транзакции
- Использование триггеров AFTER ROW и переходных таблиц удорожает обработку Все хорошо в меру: сложную логику трудно отлаживать из-за неявного выполнения триггеров
Page 23: Практика
- Создайте триггер, обрабатывающий обновление поля onhand_qty представления catalog_v.
- Проверьте, что в «Каталоге» появилась возможность заказывать книги. 2. Обеспечьте выполнение требования согласованности: количество книг на складе не может быть отрицательным (нельзя купить книгу, которой нет в наличии).
- Внимательно проверьте правильность реализации, учитывая,
- что с приложением могут одновременно работать несколько
- пользователей.
Комментарии преподавателя
- Может показаться, что достаточно создать AFTER-триггер на таблице operations, подсчитывающий сумму qty_change. Однако на уровне изоляции Read Committed, с которым работает приложение «Книжный магазин», нам придется блокировать таблицу operations в эксклюзивном режиме — иначе возможны сценарии, при которых такая проверка не сработает.
Лучше поступить следующим образом: добавить в таблицу books поле onhand_qty и создать триггер, изменяющий это поле при изменении таблицы operations (то есть, фактически, выполнить денормализацию данных). На поле onhand_qty теперь можно наложить ограничение CHECK, реализующее требование согласованности. А функция onhand_qty(), которую мы создавали ранее, больше не нужна.
Особое внимание надо уделить начальной установке значения, учитывая, что одновременно с выполнением наших операций в системе могут работают пользователи.
Page 24: 1. Триггер для обновления каталога
2. Проверка количества книг
Добавляем к таблице книг поле наличного количества. (До версии 11 важно было учитывать, что указание предложения DEFAULT вызывало перезапись всех строк таблицы, удерживая блокировку.)
Триггерная функция для AFTER-триггера на вставку для обновления количества (предполагаем, что поле onhand_qty не может быть пустым):
Дальше все происходит внутри транзакции.
Блокируем операции на время транзакции:
Начальное заполнение:
Теперь, когда поле заполнено, задаем ограничения:
Page 25: Page 25
Создаем триггер:
Готово.
Теперь books.onhand_qty обновляется, но представление catalog_v по-прежнему вызывает функцию для подсчета количества. Хоть в исходном запросе обращение к функции синтаксически не отличается от обращения к полю, запрос был запомнен в другом виде:
Поэтому пересоздадим представление:
Теперь функцию можно удалить.
Небольшая проверка:
Page 26: Page 26
Некорректные операции обрываются:
Page 27: Практика+
- Напишите триггер, увеличивающий счетчик (поле version) на единицу при каждом изменении строки. При вставке новой строки счетчик должен устанавливаться в единицу. Проверьте правильность работы. 2. Даны таблицы заказов (orders) и строк заказов (lines). Требуется выполнить денормализацию: автоматически обновлять сумму заказа в таблице orders при изменении строк в заказе.
- Создайте необходимые триггеры с использованием
- переходных таблиц для минимизации операций обновления.
Комментарии преподавателя
- Для создания таблиц используйте команды:
Столбец orders.total_amount должен автоматически вычисляться как сумма значений столбца lines.amount всех строк, относящихся к соответствующему заказу.
Page 28: 1. Счетчик номера версии
Таблица:
Триггерная функция:
Триггер:
Проверяем:
Явное указание version игнорируется:
Изменение:
Page 29: Page 29
Явное указание также игнорируется:
2. Автоматическое вычисление общей суммы заказов
Создаем таблицы упрощенной структуры, достаточной для демонстрации:
Создаем триггерную функцию и триггер для обработки вставки:
Предложение FROM в команде UPDATE позволяет соединить orders с подзапросом по переходной таблице и использовать столбцы подзапроса для вычисления значения.
Функция и триггер для обработки обновления:
Page 30: Page 30
Условие HAVING позволяет пропускать изменения, не влияющие на общую сумму заказа.
Функция и триггер для обработки удаления:
Остался неохваченным оператор TRUNCATE. Однако триггер для этого оператора не может использовать переходные таблицы. Но мы знаем, что после выполнения TRUNCATE в lines не останется строк, значит можно обнулить суммы всех заказов.
Page 31: Page 31
Дополнительно нужно запретить изменять значение total_amount вручную, но это задача решается не триггерами. Проверяем работу. Добавили два новых заказа без строк:
Добавили строки в заказы:
Удвоили суммы всех строк всех заказов:
Удалим одну строку первого заказа:
Опустошим таблицу строк: