38.7. Сравнение правил и триггеров

Многие вещи, которые можно сделать с помощью триггеров, можно также реализовать, используя систему правил Postgres Pro. Однако, используя правила, нельзя реализовать, например, некоторые типы ограничений, в частности, внешние ключи. Хотя можно определить правило с ограничивающим условием, которое будет преобразовать команду в NOTHING, если значение ключа не находится в другой таблице, но при этом неподходящие данные будут отбрасываться молча, а это не самый лучший вариант. Также, если требуется проверить правильность значений и, обнаружив неверное значение, выдать ошибку, это нужно делать в триггере.

В этой главе мы разберём использование правил для изменения представлений. Все правила, приведённые в примерах этой главы, можно также заменить триггерами INSTEAD OF для представлений. Написать такие триггеры часто бывает проще, чем разработать правила, особенно если для изменений применяется сложная логика.

Для тех задач, которые можно решить обоими способами, лучший выбирается в зависимости от характера использования базы данных. Следует учитывать, что триггер срабатывает для каждой обрабатываемой строки, а правило изменяет существующий запрос или создаёт ещё один. Поэтому, если один оператор обрабатывает сразу много строк, правило, добавляющее дополнительную команду, скорее всего, будет работать быстрее, чем триггер, который вызывается для каждой очередной строки и должен каждый раз определять, что с ней делать. Однако триггеры концептуально гораздо проще правил, и использовать их правильно новичкам гораздо проще.

Давайте рассмотрим пример, показывающий, как выбор в пользу правил вместо триггеров оказывается выигрышным в определённой ситуации. Пусть у нас есть две таблицы:

CREATE TABLE computer (
    hostname        text,    -- индексированное
    manufacturer    text     -- индексированное
);

CREATE TABLE software (
    software        text,    -- индексированное
    hostname        text     -- индексированное
);

Обе таблицы содержат несколько тысяч строк, а индексы по полю hostname являются уникальными. Правило или триггер должны реализовать ограничение, которое удалит строки из таблицы software, ссылающиеся на удаляемый компьютер. Триггер выполнял бы такую команду:

DELETE FROM software WHERE hostname = $1;

Так как триггер вызывается для каждой отдельной строки, удаляемой из таблицы computer, он может подготовить и сохранить план этой команды, а затем передавать значение hostname подготовленному запросу в параметрах. Правило же можно записать так:

CREATE RULE computer_del AS ON DELETE TO computer
    DO DELETE FROM software WHERE hostname = OLD.hostname;

Теперь давайте взглянем на разные варианты удаления. В этом случае:

DELETE FROM computer WHERE hostname = 'mypc.local.net';

таблица computer сканируется по индексу (быстро), и команда, выполняемая триггером, так же будет применять сканирование по индексу (тоже быстро). Дополнительной командой правила будет:

DELETE FROM software WHERE computer.hostname = 'mypc.local.net'
                       AND software.hostname = computer.hostname;

Так как созданы все необходимые индексы, планировщик создаст план

Nestloop
  ->  Index Scan using comp_hostidx on computer
  ->  Index Scan using soft_hostidx on software

Таким образом, большого различия в скорости между реализациями с триггером и с правилом не будет.

Теперь мы хотим избавиться от 2000 компьютеров, у которых hostname начинается с old. Это можно сделать двумя командами. Первая:

DELETE FROM computer WHERE hostname >= 'old'
                       AND hostname <  'ole'

Правило преобразует её в:

DELETE FROM software WHERE computer.hostname >= 'old' AND computer.hostname < 'ole'
                       AND software.hostname = computer.hostname;

с планом:

Hash Join
  ->  Seq Scan on software
  ->  Hash
    ->  Index Scan using comp_hostidx on computer

С другой возможной командой:

DELETE FROM computer WHERE hostname ~ '^old';

для запроса, преобразованного правилом, получается следующий план:

Nestloop
  ->  Index Scan using comp_hostidx on computer
  ->  Index Scan using soft_hostidx on software

Это показывает, что планировщик не понимает, что ограничение по hostname в computer можно также использовать для сканирования по индексу в software, когда несколько условий объединяются с помощью AND, что он успешно делает для варианта команды с регулярным выражением. Триггер будет вызываться для каждой из 2000 удаляемых записей о старых компьютерах, и это приведёт к одному сканированию индекса в таблице computer и 2000 сканированиям индекса в таблице software. Реализация с правилом делает это двумя командами, применяющими индексы. Будет ли правило быстрее при последовательном сканировании, зависит от общего размера таблицы software. С другой стороны, выполнение 2000 команд из триггера через менеджер SPI всё равно займёт время, даже если все блоки индекса вскоре окажутся в кеше.

В завершение взгляните на эту команду:

DELETE FROM computer WHERE manufacturer = 'bim';

Она также может привести к удалению множества строк из таблицы computer. Поэтому триггер снова пропустит через исполнитель такое же множество команд. Правило же выдаст следующую команду:

DELETE FROM software WHERE computer.manufacturer = 'bim'
                       AND software.hostname = computer.hostname;

План для этой команды снова будет содержать вложенный цикл по двум сканированиям индекса, но на этот раз с другим индексом таблицы computer:

Nestloop
  ->  Index Scan using comp_manufidx on computer
  ->  Index Scan using soft_hostidx on software

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

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

8.21. Pseudo-Types

The Postgres Pro type system contains a number of special-purpose entries that are collectively called pseudo-types. A pseudo-type cannot be used as a column data type, but it can be used to declare a function's argument or result type. Each of the available pseudo-types is useful in situations where a function's behavior does not correspond to simply taking or returning a value of a specific SQL data type. Table 8.27 lists the existing pseudo-types.

Table 8.27. Pseudo-Types

NameDescription
anyIndicates that a function accepts any input data type.
anyelementIndicates that a function accepts any data type (see Section 37.2.5).
anyarrayIndicates that a function accepts any array data type (see Section 37.2.5).
anynonarrayIndicates that a function accepts any non-array data type (see Section 37.2.5).
anyenumIndicates that a function accepts any enum data type (see Section 37.2.5 and Section 8.7).
anyrangeIndicates that a function accepts any range data type (see Section 37.2.5 and Section 8.17).
anymultirangeIndicates that a function accepts any multirange data type (see Section 37.2.5 and Section 8.17).
anycompatibleIndicates that a function accepts any data type, with automatic promotion of multiple arguments to a common data type (see Section 37.2.5).
anycompatiblearrayIndicates that a function accepts any array data type, with automatic promotion of multiple arguments to a common data type (see Section 37.2.5).
anycompatiblenonarrayIndicates that a function accepts any non-array data type, with automatic promotion of multiple arguments to a common data type (see Section 37.2.5).
anycompatiblerangeIndicates that a function accepts any range data type, with automatic promotion of multiple arguments to a common data type (see Section 37.2.5 and Section 8.17).
anycompatiblemultirangeIndicates that a function accepts any multirange data type, with automatic promotion of multiple arguments to a common data type (see Section 37.2.5 and Section 8.17).
cstringIndicates that a function accepts or returns a null-terminated C string.
internalIndicates that a function accepts or returns a server-internal data type.
language_handlerA procedural language call handler is declared to return language_handler.
fdw_handlerA foreign-data wrapper handler is declared to return fdw_handler.
table_am_handlerA table access method handler is declared to return table_am_handler.
index_am_handlerAn index access method handler is declared to return index_am_handler.
tsm_handlerA tablesample method handler is declared to return tsm_handler.
recordIdentifies a function taking or returning an unspecified row type.
triggerA trigger function is declared to return trigger.
event_triggerAn event trigger function is declared to return event_trigger.
pg_ddl_commandIdentifies a representation of DDL commands that is available to event triggers.
voidIndicates that a function returns no value.
unknownIdentifies a not-yet-resolved type, e.g., of an undecorated string literal.

Functions coded in C (whether built-in or dynamically loaded) can be declared to accept or return any of these pseudo-types. It is up to the function author to ensure that the function will behave safely when a pseudo-type is used as an argument type.

Functions coded in procedural languages can use pseudo-types only as allowed by their implementation languages. At present most procedural languages forbid use of a pseudo-type as an argument type, and allow only void and record as a result type (plus trigger or event_trigger when the function is used as a trigger or event trigger). Some also support polymorphic functions using the polymorphic pseudo-types, which are shown above and discussed in detail in Section 37.2.5.

The internal pseudo-type is used to declare functions that are meant only to be called internally by the database system, and not by direct invocation in an SQL query. If a function has at least one internal-type argument then it cannot be called from SQL. To preserve the type safety of this restriction it is important to follow this coding rule: do not create any function that is declared to return internal unless it has at least one internal argument.