Триггеры и события

Рейтинг: 62.3% · 11 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Триггеры и события

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события (вы здесь)
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 31. Триггеры и события

Триггер - это код, который PostgreSQL запускает сам, без вашего явного вызова, в ответ на изменение данных или на команду. Вы один раз описываете правило ("при каждом UPDATE цены пиши строку в журнал"), и дальше база соблюдает его за вас, кто бы ни выполнял запрос - приложение, отчёт, ручной psql или внешняя интеграция вроде postgresql для 1с. В этом уроке разберём, из чего состоит триггер, чем отличаются BEFORE/AFTER/INSTEAD OF и ROW/STATEMENT, что лежит в записях NEW и OLD, где триггеры реально полезны (аудит, денормализация, проверки), что такое событийные триггеры на DDL и почему триггеры легко превращаются в тормоз.

Изображение

Как это работает

Триггер в PostgreSQL - это связка двух сущностей. Сначала вы пишете триггерную функцию: обычную функцию на PL/pgSQL (или другом языке) с типом возврата trigger. Затем командой CREATE TRIGGER привязываете её к таблице и говорите, на какое событие и в какой момент её звать. Одну функцию можно повесить на несколько таблиц - внутри она узнает контекст через служебные переменные.

Момент срабатывания задаёт суть. BEFORE-триггер выполняется до того, как строка реально записана, поэтому в нём можно поправить или отменить изменение. AFTER-триггер срабатывает после записи, когда новое состояние уже зафиксировано в рамках транзакции, - это место для аудита и каскадных действий. INSTEAD OF существует только для представлений (VIEW): обычная вьюха не принимает запись напрямую, а такой триггер перехватывает INSERT/UPDATE/DELETE и раскладывает их по настоящим таблицам.

Второе важное деление - уровень. Строчный триггер (FOR EACH ROW) вызывается на каждую затронутую строку: обновили 5000 строк - функция отработает 5000 раз. Операторный (FOR EACH STATEMENT) срабатывает ровно один раз на всю команду, даже если она не задела ни одной строки. Строчный нужен, когда логика зависит от конкретных значений; операторный - когда хватает факта "команда была".

Внутри строчной функции доступны NEW и OLD - записи типа таблицы. На INSERT есть только NEW (новая строка), на DELETE - только OLD (удаляемая), на UPDATE - обе. В BEFORE-триггере можно менять поля NEW, и эти правки попадут в таблицу. Возврат тоже значим: BEFORE ROW должен вернуть NEW (или изменённую запись), чтобы операция продолжилась, либо NULL - чтобы тихо отменить её для этой строки. У AFTER-триггера возвращаемое значение игнорируется. Контекст подскажут переменные TG_OP (INSERT/UPDATE/DELETE), TG_TABLE_NAME, TG_WHEN и другие.

Событийные триггеры (event triggers) - отдельный механизм, реагирующий не на данные, а на сами команды DDL: создание таблицы, ALTER, DROP. Они ловятся на события ddl_command_start, ddl_command_end, sql_drop и table_rewrite и помогают вести аудит изменений схемы или запрещать опасные операции на проде.

SQL и примеры

Аудит изменения цены билета в схеме bookings. Сначала журнал и триггерная функция, которая пишет в него старую и новую сумму:

Код: Выделить всё

CREATE TABLE bookings.fare_audit (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    ticket_no   char(13),
    flight_id   integer,
    old_amount  numeric(10,2),
    new_amount  numeric(10,2),
    changed_by  text    DEFAULT current_user,
    changed_at  timestamptz DEFAULT now()
);

CREATE OR REPLACE FUNCTION bookings.log_fare_change()
RETURNS trigger AS $$
BEGIN
    IF NEW.amount IS DISTINCT FROM OLD.amount THEN
        INSERT INTO bookings.fare_audit
               (ticket_no, flight_id, old_amount, new_amount)
        VALUES (OLD.ticket_no, OLD.flight_id, OLD.amount, NEW.amount);
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Привязываем её как AFTER UPDATE на конкретный столбец - так триггер не дёргается на правках других полей:

Код: Выделить всё

CREATE TRIGGER trg_fare_audit
    AFTER UPDATE OF amount ON bookings.ticket_flights
    FOR EACH ROW
    EXECUTE FUNCTION bookings.log_fare_change();
Здесь IS DISTINCT FROM сравнивает значения корректно даже при NULL (обычное <> на NULL даёт NULL, и условие не сработает). GENERATED ALWAYS AS IDENTITY - современная замена устаревшего serial.

Проверка-валидация в BEFORE-триггере: запретим отрицательную сумму, не доводя дело до записи:

Код: Выделить всё

CREATE OR REPLACE FUNCTION bookings.check_amount()
RETURNS trigger AS $$
BEGIN
    IF NEW.amount < 0 THEN
        RAISE EXCEPTION 'Сумма не может быть отрицательной: %', NEW.amount;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_check_amount
    BEFORE INSERT OR UPDATE ON bookings.ticket_flights
    FOR EACH ROW
    EXECUTE FUNCTION bookings.check_amount();
Операторный триггер для агрегата: один раз на команду фиксируем факт массовой правки рейсов. Чтобы увидеть набор изменённых строк целиком, в PG 10+ есть transition tables:

Код: Выделить всё

CREATE OR REPLACE FUNCTION bookings.count_flight_updates()
RETURNS trigger AS $$
DECLARE n bigint;
BEGIN
    SELECT count(*) INTO n FROM changed;
    RAISE NOTICE 'Обновлено рейсов: %', n;
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_flights_bulk
    AFTER UPDATE ON bookings.flights
    REFERENCING NEW TABLE AS changed
    FOR EACH STATEMENT
    EXECUTE FUNCTION bookings.count_flight_updates();
Событийный триггер - запретить DROP TABLE в рабочее время:

Код: Выделить всё

CREATE OR REPLACE FUNCTION guard_drops()
RETURNS event_trigger AS $$
BEGIN
    RAISE EXCEPTION 'DROP запрещён на этом сервере';
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER no_drop ON sql_drop
    EXECUTE FUNCTION guard_drops();
Частые грабли
  • Изменять NEW в AFTER-триггере бессмысленно: строка уже записана, правки не применятся. Меняйте только в BEFORE ROW.
  • Забыть RETURN NEW в BEFORE ROW - и операция тихо отменится (вернётся NULL), строка не сохранится, ошибки не будет.
  • Тяжёлая логика в FOR EACH ROW на пакетном UPDATE: функция вызывается на каждую строку. На загрузке миллионов строк это умножает время в разы - берите FOR EACH STATEMENT с transition tables.
  • Триггер, который UPDATE-ит ту же таблицу, способен запустить сам себя - рекурсия. Защищайтесь условием выхода или pg_trigger_depth().
  • Порядок строчных триггеров одного события - по алфавиту имён, а не по дате создания. Называйте осознанно, если порядок важен.
  • Триггеры не видны в EXPLAIN плана, но их время показывает EXPLAIN ANALYZE отдельной строкой Trigger ... time - проверяйте её при разборе медленных вставок.
  • По умолчанию триггеры не срабатывают на действия от внешних ключей и на COPY-логику репликации; для логической репликации нужен ENABLE ALWAYS/REPLICA TRIGGER.
Мини-лаба
  • Создайте таблицу аудита и триггерную функцию из примера, привяжите AFTER UPDATE OF amount к bookings.ticket_flights.
  • Выполните UPDATE bookings.ticket_flights SET amount = amount + 100 WHERE ticket_no = (SELECT ticket_no FROM bookings.ticket_flights LIMIT 1) и проверьте, что в fare_audit появилась запись.
  • Сделайте UPDATE того же билета без смены суммы (например SET fare_conditions = fare_conditions) и убедитесь, что новой строки в аудите нет.
  • Добавьте BEFORE-триггер проверки и попробуйте записать отрицательную сумму - поймайте RAISE EXCEPTION.
  • Оберните UPDATE и проверку в одну транзакцию, сделайте ROLLBACK и убедитесь, что аудит откатился вместе с данными.
  • Через EXPLAIN ANALYZE на UPDATE найдите строку Trigger и оцените, сколько времени съел триггер.
  • Временно отключите триггер через ALTER TABLE ... DISABLE TRIGGER и проверьте, что аудит перестал писаться.
Контрольные вопросы
  • Чем BEFORE-триггер принципиально отличается от AFTER, и в каком из них можно отменить или изменить строку?
  • Что лежит в NEW и OLD при INSERT, UPDATE и DELETE и какое значение должна вернуть BEFORE ROW функция?
  • Когда выгоднее FOR EACH STATEMENT вместо FOR EACH ROW и как тогда увидеть изменённые строки?
  • Почему IS DISTINCT FROM надёжнее обычного сравнения при отслеживании изменений столбца?
  • Зачем нужны event triggers и на какие события DDL их можно повесить?
  • Как триггер может уйти в рекурсию и какими способами это предотвратить?
👍3 ❤️4 🔥1 😄 🤔
Аватара пользователя
KotlinFan
Сообщения: 1
Зарегистрирован: 03 июн 2026, 21:18

Re: Триггеры и события

Сообщение KotlinFan »

А если на таблице висят и BEFORE и AFTER на UPDATE - в каком порядке они отработают относительно друг друга? По именам алфавитно или сначала все BEFORE, потом запись, потом все AFTER?
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
lovemachine
Сообщения: 1
Зарегистрирован: 14 май 2026, 10:12

Re: Триггеры и события

Сообщение lovemachine »

Поймал на проде: писал аудит в FOR EACH ROW, на ночной загрузке 4 млн строк вставка раздулась с минуты до получаса. Переделал на STATEMENT с transition table - и время вернулось.
👍1 ❤️ 🔥1 😄 🤔1
Ответить
← Предыдущая глава
Серверное программирование: функции и процедуры
Следующая глава →
Архитектура PostgreSQL: процессы и память

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
  • Похожие темы
Похожие запросы: триггеры и хранимые процедуры pl/pgsql в postgresql

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость