Триггеры

Эта глава содержит общую информацию о написании триггерных функций. Триггерные функции могут быть написаны на большинстве доступных процедурных языков, включая PL/pgSQL. Прочитав текущую главу, вы можете обратиться к главе PL/pgSQL, чтобы узнать подробности о написании на нем триггера.

Также можно написать триггерную функцию на C или Rust, хотя большинству людей проще использовать один из процедурных языков. В настоящее время невозможно написать триггерную функцию на простом языке функций SQL.

Обзор триггерного поведения

Триггер - это указание, согласно которой база данных должна автоматически выполнять определенную функцию всякий раз, когда выполняется определенный тип операции. Триггеры могут быть прикреплены к таблицам (разделенным или нет), представлениям и сторонним таблицам.

В таблицах и внешних таблицах можно определить триггеры для выполнения до или после любой операции INSERT, UPDATE или DELETE, либо один раз для измененной строки, либо один раз для оператора SQL. Триггеры UPDATE могут быть установлены на срабатывание только в том случае, если в предложении SET UPDATE упоминаются определенные столбцы. Триггеры также могут быть TRUNCATE. Если происходит событие триггера, функция триггера вызывается в соответствующее время для обработки события.

В представлениях можно определить триггеры для выполнения вместо операций INSERT, UPDATE или DELETE. Такие триггеры INSTEAD OF запускаются один раз для каждой строки, которую необходимо изменить. Функция триггера отвечает за выполнение необходимых модификаций базовой таблицы (или таблиц) представления и при необходимости возвращает измененную строку, как она будет отображаться в представлении. Триггеры для представлений также могут быть определены для выполнения один раз для каждого оператора SQL, до или после операций INSERT, UPDATE или DELETE. Однако такие триггеры срабатывают только в том случае, если в представлении также имеется триггер INSTEAD OF. В противном случае любой оператор, нацеленный на представление должен быть переписан в оператор, влияющий на его базовую(ые) таблицу(ы), и тогда триггеры, которые будут срабатывать.

Функция триггера должна быть определена до создания триггера и объявлена как функция, не имеющая аргументов и возвращающая тип trigger. Функция триггера получает свой ввод через специально переданную структуру TriggerData, а не в виде аргументов обычной функции.

После создания подходящей триггерной функции триггер устанавливается с помощью CREATE TRIGGER. Одна и та же триггерная функция может использоваться для нескольких триггеров.

QHB предлагает как триггеры для каждой строки, так и триггеры для каждой инструкции. При использовании триггера, функция вызывается один раз для каждой строки, на которую влияет оператор, вызвавший триггер. Напротив, триггер для каждого оператора вызывается только один раз при выполнении, независимо от количества строк, затронутых этим оператором. В частности, оператор, который влияет на ноль строк, все равно будет приводить к выполнению любых применимых триггеров для каждого оператора. Эти два типа триггеров иногда называют триггерами уровня строки и триггерами уровня оператора, соответственно. Триггеры в TRUNCATE могут быть определены только на уровне оператора, а не для каждой строки.

Триггеры также классифицируются в зависимости от того, срабатывают они до, после или вместо операции. Они называются триггерами BEFORE, триггерами AFTER и триггерами INSTEAD OF соответственно. Триггеры уровня BEFORE естественным образом срабатывают до того, как оператор начинает что-либо делать, в то время как AFTER запускаются на самом конце оператора. Эти типы триггеров могут быть определены в таблицах, представлениях или внешних таблицах. Уровень BEFORE вызывает срабатывание непосредственно перед тем, как будет обработана определенная строка, в то время как уровень AFTER срабатывает в конце инструкции. Эти типы триггеров могут быть определены только для секционированных и внешних таблиц, но не для представлений. Триггеры INSTEAD OF могут быть определены только для представлений и только на уровне строк; они запускаются немедленно, так как каждая строка в представлении определяется как нуждающаяся в операции.

Оператор, нацеленный на родительскую таблицу в иерархии наследования или разбиения, не вызывает срабатывания триггеров уровня дочерних таблиц (запускаются только триггеры уровня операторов) родительской таблицы. Однако триггеры на уровне строк любых затронутых дочерних таблиц будут срабатывать.

Если INSERT содержит предложение ON CONFLICT DO UPDATE, возможно, что эффекты триггеров BEFORE INSERT и BEFORE UPDATE уровня строки могут быть применены так, как это видно из конечного состояния обновленной строки, если ссылка на столбец была EXCLUDED. Однако для выполнения обоих наборов триггеров уровня BEFORE необязательно указывать ссылку на столбец EXCLUDED. Возможность неожиданных результатов должна быть рассмотрена, когда есть триггеры уровня строки BEFORE INSERT и BEFORE UPDATE, которые изменяют вставляемую / обновляемую строку (это может быть проблематично, даже если изменения более или менее эквивалентны, или даже идемпотентны). Обратите внимание, что триггеры UPDATE на уровне оператора выполняются, когда указано ON CONFLICT DO UPDATE, независимо от того, затронул ли UPDATE какие-либо строки или нет, и независимо от того, выбирался ли альтернативный путь UPDATE. INSERT с предложением ON CONFLICT DO UPDATE сначала выполнит триггеры уровня BEFORE INSERT уровня INSERT, затем триггеры BEFORE UPDATE, затем триггеры AFTER UPDATE и, наконец, триггеры AFTER INSERT.

Если UPDATE в многораздельной таблице приводит к перемещению строки в другой раздел, он будет выполнен как DELETE из исходного раздела, за которым следует INSERT в новый раздел. В этом случае все триггеры BEFORE UPDATE уровня строки и все триггеры BEFORE DELETE уровня строки запускаются в исходном разделе. Затем все триггеры BEFORE INSERT уровня строки запускаются в целевом разделе. Возможность неожиданных результатов следует учитывать, когда все эти триггеры влияют на перемещение строки. Что касается триггеров AFTER ROW применяются триггеры AFTER DELETE и AFTER INSERT; но не AFTER UPDATE, потому что UPDATE было преобразовано в DELETE и INSERT. Что касается триггеров уровня оператора, ни один из триггеров DELETE или INSERT не запускается, даже если происходит перемещение строки; сработают только триггеры UPDATE определенные в целевой таблице, используемой в операторе UPDATE.

Триггерные функции, вызываемые триггерами для каждого оператора, всегда должны возвращать NULL. Триггерные функции, вызываемые триггерами для каждой строки, могут возвращать строку таблицы (значение типа HeapTuple) вызывающему исполнителю, если они того пожелают. Триггер на уровне строк, срабатывающий до операции, имеет следующие варианты:

  • Он может вернуть NULL, чтобы пропустить операцию для текущей строки. Это указывает исполнителю не выполнять операцию на уровне строк, которая вызвала триггер (вставка, изменение или удаление определенной строки таблицы).

  • Только для триггеров INSERT и UPDATE на уровне строк возвращаемая строка становится строкой, которая будет вставлена или заменит обновляемую строку. Это позволяет триггерной функции изменять вставляемую или обновляемую строку.

Триггер BEFORE уровня строки, который не имеет намерения вызвать какое-либо из этих поведений, должен быть аккуратен. В таком случае необходимо возвращать в качестве своего результата ту же самую строку, которая была передана. (то есть строка NEW для триггеров INSERT и UPDATE, строка OLD для триггеров DELETE).

Триггер INSTEAD OF уровня строк должен либо возвращать NULL, чтобы указать, что он не изменил никаких данных из базовых таблиц представления, либо возвращать строку представления, которая была передана (строка NEW для операций INSERT и UPDATE, или OLD ряд для DELETE операции). Ненулевое возвращаемое значение используется, чтобы сигнализировать, что триггер выполнил необходимые модификации данных в представлении. Это приведет к увеличению числа строк, на которые влияет команда. Для операций INSERT и UPDATE триггер может изменить строку NEW перед ее возвратом. Это изменит данные, возвращаемые INSERT RETURNING или UPDATE RETURNING, и будет полезно, когда представление не будет отображать точно те же данные, которые были предоставлены.

Возвращаемое значение игнорируется для триггеров уровня строки, срабатывающих после операции, и поэтому они могут возвращать NULL.

Некоторые соображения применимы к сгенерированным столбцам. Сохраненные сгенерированные столбцы вычисляются после триггеров BEFORE и до триггеров AFTER. Поэтому сгенерированное значение может быть проверено в триггерах AFTER. В триггерах BEFORE строка OLD содержит старое сгенерированное значение, как и следовало ожидать, но строка NEW еще не содержит нового сгенерированного значения и не должна быть доступна. В интерфейсе языка C содержимое столбца на этом этапе не определено; язык программирования более высокого уровня должен предотвращать доступ к сохраненному сгенерированному столбцу в строке NEW в триггере BEFORE. Изменения значения сгенерированного столбца в триггере BEFORE игнорируются и будут перезаписаны.

Если для одного и того же события в одном и том же отношении определено более одного триггера, триггеры сработают в алфавитном порядке по имени триггера. В случае триггеров BEFORE и INSTEAD OF возможно измененная строка, возвращаемая каждым триггером, становится входом для следующего триггера. Если какой-либо триггер BEFORE или INSTEAD OF возвращает NULL, операция прекращается для этой строки, и последующие триггеры не запускаются (для этой строки).

Определение триггера также может указывать логическое условие WHEN, которое будет проверено, чтобы увидеть, должен ли триггер срабатывать. В триггерах уровня строки условие WHEN может проверять старые и (или) новые значения столбцов строки. (Триггеры уровня оператора также могут иметь условия WHEN, хотя для них эта функция не так полезна.) В триггере BEFORE условие WHEN оценивается непосредственно перед выполнением функции, поэтому использование WHEN существенно не отличается от проверки того же условия в начале триггерной функции. Однако в триггере AFTER условие WHEN оценивается сразу после обновления строки и определяет, стоит ли в очереди событие для запуска триггера в конце оператора. Таким образом, когда условие WHEN триггера AFTER не возвращает истину, нет необходимости ставить в очередь событие или повторно извлекать строку в конце оператора. Это может привести к значительному ускорению операторов, которые изменяют множество строк, если триггер нужно запустить только для нескольких строк. INSTEAD OF не поддерживают условия WHEN.

Как правило, триггеры BEFORE уровня строки используются для проверки или изменения данных, которые будут вставлены или обновлены. Например, триггер BEFORE может использоваться для вставки текущего времени в столбец timestamp или для проверки согласованности двух элементов строки. Триггеры AFTER наиболее разумно используются для распространения обновлений на другие таблицы или для проверки согласованности с другими таблицами. Причина такого разделения заключается в том, что триггер AFTER должен быть уверен, что видит окончательное значение строки, а триггер BEFORE нет; после этого могут быть другие BEFORE триггерами. Если у вас нет особой причины для выполнения триггера BEFORE или AFTER, случай BEFORE более эффективен, поскольку информацию об операции не нужно сохранять до конца оператора.

Если триггерная функция выполняет команды SQL, эти команды могут снова запускать триггеры. Это известно как каскадные триггеры. Нет прямого ограничения на количество каскадных уровней. Каскады могут вызывать рекурсивный вызов одного и того же триггера; например, триггер INSERT может выполнить команду, которая вставляет дополнительную строку в ту же таблицу, вызывая повтор триггера INSERT. Программист триггера обязан избегать бесконечной рекурсии в таких сценариях.

Когда определяется триггер, для него могут быть указаны аргументы. Цель включения аргументов в определение триггера - разрешить различным триггерам с одинаковыми требованиями вызывать одну и ту же функцию. Например, может быть обобщенная триггерная функция, которая принимает в качестве аргументов два имени столбца и помещает текущего пользователя в один, а текущую метку времени - в другой. Правильно написанная триггерная функция не зависит от конкретной таблицы, для которой она запускается. Таким образом, эту же функцию можно использовать для событий INSERT в любой таблице с подходящими столбцами, например, для автоматического отслеживания создания записей в таблице транзакций. Он также может использоваться для отслеживания событий последнего обновления, если он определен как триггер UPDATE.

Каждый язык программирования, который поддерживает триггеры, имеет свой собственный метод, позволяющий сделать входные данные триггера доступными для функции триггера. Эти входные данные включают тип события триггера (например, INSERT или UPDATE), а также любые аргументы, которые были перечислены в CREATE TRIGGER. Для триггера уровня строки входные данные также включают строку NEW для триггеров INSERT и UPDATE и (или) или строку OLD для триггеров UPDATE и DELETE.

По умолчанию триггеры уровня оператора не имеют никакого способа проверить отдельные строки, измененные этим самым оператором. Но триггер AFTER STATEMENT может запросить создание таблиц переходов, чтобы сделать наборы затронутых строк доступными для триггера. AFTER ROW также могут запрашивать таблицы переходов, чтобы они могли видеть общие изменения в таблице, а также изменения в отдельной строке, для которой они в данный момент запускаются. Способ проверки таблиц переходов тоже зависит от используемого языка программирования, но типичный подход заключается в том, чтобы заставить таблицы переходов действовать как временные таблицы только для чтения, к которым могут обращаться команды SQL, выполненные в функции триггера.

Видимость изменений данных

Если вы выполняете команды SQL в своей функции триггера, и эти команды обращаются к таблице, вам необходимо знать правила видимости данных, поскольку они определяют, будут ли эти команды SQL видеть изменение данных, для которых сработал триггер. Кратко:

  • Триггеры уровня оператора следуют простым правилам видимости: ни одно из изменений, внесенных оператором, не видно триггерам BEFORE уровня оператора, тогда как все модификации видны триггерам AFTER уровня оператора.

  • Изменение данных (вставка, обновление или удаление), вызывающее срабатывание триггера, естественно, невидимо для команд SQL, выполняемых в триггере BEFORE уровне строк, поскольку это еще не произошло.

  • Однако команды SQL, выполняемые в триггере BEFORE на уровне строк, увидят влияние изменений данных для строк, ранее обработанных в той же внешней команде. Это требует осторожности, поскольку порядок этих событий изменений в общем случае не предсказуем; команда SQL, которая влияет на несколько строк, может посещать строки в любом порядке.

  • Аналогично, триггер INSTEAD OF уровня строк будет видеть влияние изменений данных, выполненных предыдущими срабатываниями триггеров INSTEAD OF в той же внешней команде.

  • Когда запускается триггер AFTER уровня строк, все изменения данных, сделанные внешней командой, уже завершены и видны для вызываемой функции триггера.

Если ваша триггерная функция написана на любом из стандартных процедурных языков, то приведенные выше операторы применимы, только если функция объявлена как VOLATILE. Функции, которые объявлены как STABLE или IMMUTABLE, не увидят изменений, внесенных вызывающей командой в любом случае.

Дополнительную информацию о правилах видимости данных можно найти в Видимость изменений данных SPI. Пример в разделе Полный пример запуска содержит демонстрацию этих правил.

Написание триггерных функций на C

В этом разделе описываются детали низкого уровня интерфейса для функции триггера. Эта информация необходима только при написании триггерных функций на языке C. Если вы используете язык более высокого уровня, то эти детали обрабатываются для вас автоматически. В большинстве случаев вам следует подумать об использовании процедурного языка, прежде чем писать свои триггеры на C. В документации каждого процедурного языка объясняется, как написать триггер на этом языке.

Функции запуска должны использовать интерфейс диспетчера функций «version 1».

Когда функция вызывается менеджером триггера, ей не передаются никакие обычные аргументы, но ей передается указатель «context», указывающий на структуру TriggerData. Функции на C могут проверить, были ли они вызваны из диспетчера триггеров или нет, выполнив макрос:

CALLED_AS_TRIGGER(fcinfo)

который распахивается в

((fcinfo)->context != NULL && IsA((fcinfo)->context, TriggerData))

Если это возвращает true, тогда безопасно привести fcinfo->context к типу TriggerData * и использовать структуру TriggerData. Функция не должна изменять структуру TriggerData или любые данные, на которые она указывает.

Структура TriggerData определена в commands/trigger.h:

typedef struct TriggerData
{
    NodeTag          type;
    TriggerEvent     tg_event;
    Relation         tg_relation;
    HeapTuple        tg_trigtuple;
    HeapTuple        tg_newtuple;
    Trigger         *tg_trigger;
    TupleTableSlot  *tg_trigslot;
    TupleTableSlot  *tg_newslot;
    Tuplestorestate *tg_oldtable;
    Tuplestorestate *tg_newtable;
} TriggerData;

где члены определены следующим образом:

  • type - всегда T_TriggerData.

  • tg_event - описывает событие, для которого вызывается функция. Вы можете использовать следующие макросы для проверки tg_event:

    • TRIGGER_FIRED_BEFORE(tg_event) - возвращает true, если триггер сработал до операции.

    • TRIGGER_FIRED_AFTER(tg_event) - возвращает true, если триггер сработал после операции.

    • TRIGGER_FIRED_INSTEAD(tg_event) - возвращает true, если сработал триггер вместо операции.

    • TRIGGER_FIRED_FOR_ROW(tg_event) - возвращает true, если триггер сработал для события уровня строки.

    • TRIGGER_FIRED_FOR_STATEMENT(tg_event) - возвращает true, если триггер сработал для события уровня оператора.

    • TRIGGER_FIRED_BY_INSERT(tg_event) возвращает true, если триггер был запущен командой INSERT.

    • TRIGGER_FIRED_BY_UPDATE(tg_event) - возвращает true, если триггер был запущен командой UPDATE.

    • TRIGGER_FIRED_BY_DELETE(tg_event) - возвращает true, если триггер был запущен командой DELETE.

    • TRIGGER_FIRED_BY_TRUNCATE(tg_event)_ - возвращает true, если триггер был запущен командой TRUNCATE.

  • tg_relation - это указатель на структуру, описывающую отношение, для которого сработал триггер. Посмотрите в utils/rel.h подробности об этой структуре. Наиболее интересными являются tg_relation->rd_att (дескриптор кортежей отношений) и tg_relation->rd_rel->relname (имя отношения; тип не char*, а NameData; используйте SPI_getrelname(tg_relation) для получения char*, если вам нужно копия имени).

  • tg_trigtuple - это указатель на строку, для которой сработал триггер. Эта строка вставляется, обновляется или удаляется. Если этот триггер был запущен для INSERT или DELETE. Это то, что вы должны вернуть из функции, если вы не хотите заменять строку другой (в случае INSERT) или пропустить операцию. Для триггеров во внешних таблицах значения системных столбцов здесь не указаны.

  • tg_newtuple - это указатель на новую версию строки, если триггер сработал для UPDATE, и NULL если он сработал для INSERT или DELETE. Это то, что вы должны вернуть из функции, если событие - UPDATE и вы не хотите заменять эту строку другой или пропустить операцию. Для триггеров во внешних таблицах значения системных столбцов здесь не указаны.

  • tg_trigger - это указатель на структуру типа Trigger, определенную в utils/reltrigger.h.

typedef struct Trigger
{
    Oid         tgoid;
    char       *tgname;
    Oid         tgfoid;
    int16       tgtype;
    char        tgenabled;
    bool        tgisinternal;
    Oid         tgconstrrelid;
    Oid         tgconstrindid;
    Oid         tgconstraint;
    bool        tgdeferrable;
    bool        tginitdeferred;
    int16       tgnargs;
    int16       tgnattr;
    int16      *tgattr;
    char      **tgargs;
    char       *tgqual;
    char       *tgoldtable;
    char       *tgnewtable;
} Trigger;

где:

  • tgname - это имя триггера.

  • tgnargs - это количество аргументов в tgargs.

  • tgargs - массив указателей на аргументы, указанные в операторе CREATE TRIGGER.

Другие члены предназначены только для внутреннего использования.

  • tg_trigtuplebuf - буфер, содержащий tg_trigtuple или InvalidBuffer если такого кортежа нет или он не хранится в дисковом буфере.

  • tg_newtuplebuf - буфер, содержащий tg_newtuple или InvalidBuffer если такого кортежа нет или он не хранится в буфере диска.

  • tg_oldtable - это указатель на структуру типа Tuplestorestate содержащую ноль или более строк в формате, заданном параметром tg_relation, или указатель NULL если нет отношения OLD TABLE.

  • tg_newtable - это указатель на структуру типа Tuplestorestate содержащую ноль или более строк в формате, заданном параметром tg_relation, или указатель NULL если нет отношения NEW TABLE.

Чтобы запросы, отправленные через SPI, ссылались на таблицы переходов, см. SPI_register_trigger_data.

Внимание! Функция триггера должна возвращать указатель NULL (но НЕ нулевое значение SQL, т.е. не устанавливать isNull как true). Будьте внимательны, возвращая либо tg_trigtuple либо tg_newtuple, в зависимости от ситуации, если вы не хотите изменять строку, с которой вы работаете.

Полный пример запуска

Вот очень простой пример триггерной функции, написанной на C. (Примеры триггеров, написанных на процедурных языках, можно найти в документации по процедурным языкам.)

Функция trigf сообщает количество строк в таблице ttest и пропускает фактическую операцию, если команда пытается вставить нулевое значение в столбец x. (Таким образом, триггер действует как ненулевое ограничение, но не прерывает транзакцию.)

Во-первых, определение таблицы:

CREATE TABLE ttest (
    x integer
);

Это исходный код функции триггера:

#include "postgres.h"
#include "fmgr.h"
#include "executor/spi.h"       /* this is what you need to work with SPI */
#include "commands/trigger.h"   /* ... triggers ... */
#include "utils/rel.h"          /* ... and relations */

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(trigf);

Datum
trigf(PG_FUNCTION_ARGS)
{
    TriggerData *trigdata = (TriggerData *) fcinfo->context;
    TupleDesc   tupdesc;
    HeapTuple   rettuple;
    char       *when;
    bool        checknull = false;
    bool        isnull;
    int         ret, i;

    /* make sure it's called as a trigger at all */
    if (!CALLED_AS_TRIGGER(fcinfo))
        elog(ERROR, "trigf: not called by trigger manager");

    /* tuple to return to executor */
    if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
        rettuple = trigdata->tg_newtuple;
    else
        rettuple = trigdata->tg_trigtuple;

    /* check for null values */
    if (!TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)
        && TRIGGER_FIRED_BEFORE(trigdata->tg_event))
        checknull = true;

    if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
        when = "before";
    else
        when = "after ";

    tupdesc = trigdata->tg_relation->rd_att;

    /* connect to SPI manager */
    if ((ret = SPI_connect()) < 0)
        elog(ERROR, "trigf (fired %s): SPI_connect returned %d", when, ret);

    /* get number of rows in table */
    ret = SPI_exec("SELECT count(*) FROM ttest", 0);

    if (ret < 0)
        elog(ERROR, "trigf (fired %s): SPI_exec returned %d", when, ret);

    /* count(*) returns int8, so be careful to convert */
    i = DatumGetInt64(SPI_getbinval(SPI_tuptable->vals[0],
                                    SPI_tuptable->tupdesc,
                                    1,
                                    &isnull));

    elog (INFO, "trigf (fired %s): there are %d rows in ttest", when, i);

    SPI_finish();

    if (checknull)
    {
        SPI_getbinval(rettuple, tupdesc, 1, &isnull);
        if (isnull)
            rettuple = NULL;
    }

    return PointerGetDatum(rettuple);
}

После того, как вы скомпилировали исходный код (см. Раздел 37.10.5), объявите функцию и триггеры:

CREATE FUNCTION trigf() RETURNS trigger
    AS 'filename'
    LANGUAGE C;

CREATE TRIGGER tbefore BEFORE INSERT OR UPDATE OR DELETE ON ttest
    FOR EACH ROW EXECUTE FUNCTION trigf();

CREATE TRIGGER tafter AFTER INSERT OR UPDATE OR DELETE ON ttest
    FOR EACH ROW EXECUTE FUNCTION trigf();

Теперь вы можете проверить работу триггера:

=> INSERT INTO ttest VALUES (NULL);
INFO:  trigf (fired before): there are 0 rows in ttest
INSERT 0 0

-- Insertion skipped and AFTER trigger is not fired

=> SELECT * FROM ttest;
 x
---
(0 rows)

=> INSERT INTO ttest VALUES (1);
INFO:  trigf (fired before): there are 0 rows in ttest
INFO:  trigf (fired after ): there are 1 rows in ttest
                                       ^^^^^^^^
                             remember what we said about visibility.
INSERT 167793 1
vac=> SELECT * FROM ttest;
 x
---
 1
(1 row)

=> INSERT INTO ttest SELECT x * 2 FROM ttest;
INFO:  trigf (fired before): there are 1 rows in ttest
INFO:  trigf (fired after ): there are 2 rows in ttest
                                       ^^^^^^
                             remember what we said about visibility.
INSERT 167794 1
=> SELECT * FROM ttest;
 x
---
 1
 2
(2 rows)

=> UPDATE ttest SET x = NULL WHERE x = 2;
INFO:  trigf (fired before): there are 2 rows in ttest
UPDATE 0
=> UPDATE ttest SET x = 4 WHERE x = 2;
INFO:  trigf (fired before): there are 2 rows in ttest
INFO:  trigf (fired after ): there are 2 rows in ttest
UPDATE 1
vac=> SELECT * FROM ttest;
 x
---
 1
 4
(2 rows)

=> DELETE FROM ttest;
INFO:  trigf (fired before): there are 2 rows in ttest
INFO:  trigf (fired before): there are 1 rows in ttest
INFO:  trigf (fired after ): there are 0 rows in ttest
INFO:  trigf (fired after ): there are 0 rows in ttest
                                       ^^^^^^
                             remember what we said about visibility.
DELETE 2
=> SELECT * FROM ttest;
 x
---
(0 rows)

Более сложные примеры доступны в src/test/regress/regress.c и SPI.