Автономные транзакции — расширение языка PL/pgSQL

”The legitimate real-world use of autonomous transactions is exceedingly rare. If you find them to be a feature you are using constantly, you’ll want to take a long, hard look at why.”

«Оправданное применение автономных транзакций в реальных задачах — исключительно редкое явление. Если вы используете их на постоянной основе, вам следует крепко призадуматься над тем, зачем вы это делаете.»

-- Tom Kyte, эксперт компании Oracle



Обзор

Автономные транзакции предоставляют особый способ управления транзакциями в хранимых процедурах. Автономная транзакция — это субтранзакция, изолированная от родительской транзакции. Автономные транзакции могут фиксировать (COMMIT) или откатывать (ROLLBACK) изменения вне зависимости от результата родительской транзакции.



Обоснование

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

  • Средство отладки. Аналогично предыдущему примеру трассировочная информация сохранится и в случае сбоя отлаживаемой транзакции.

  • Упрощение бизнес-логики в некоторых сценариях. Например, выполнение ряда операций (оповещение, обновление) без риска потери всех результатов в случае отката основной транзакции.

Следует иметь в виду, что применение автономных транзакций связано с повышенной нагрузкой на базу данных; оно требует дополнительного расхода ресурсов, возникает необходимость переключения контекста. Может повыситься логическая сложность приложения, что затруднит сопровождение. Лучше избегать автономных транзакций, а если они все-таки необходимы — ограничиваться простыми моделями использования.



Использование

Главный синтаксический блок кода хранимой процедуры (включая функции, анонимные блоки и триггеры) может быть помечен необязательным ключевым словом AUTONOMOUS. Это означает, что функция целиком выполняется в единой неявной автономной транзакции. Такую функцию будем называть автономной функцией. Автономные функции могут быть вложенными; в этом случае фоновый процесс автономного сеанса запускает фоновый процесс нового автономного сеанса. Заметим, что только основной блок BEGIN..END функции может быть AUTONOMOUS. Попытка сделать автономным любой другой блок кода приведет к синтаксической ошибке.

Простейший пример использования (при условии, что параметр autonomous_pool_capacity уже установлен в ненулевое значение):

CREATE TABLE test_table (a int);

-- Определение функции, содержащей автономную транзакцию
CREATE OR REPLACE FUNCTION func() RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN AUTONOMOUS
    INSERT INTO test_table VALUES (1);
    INSERT INTO test_table VALUES (2);
END;
$$;

-- Выполнение функции и откат родительской транзакции
START TRANSACTION;
SELECT func();
ROLLBACK;

-- Проверка того, что автономная субтранзакция была зафиксирована
SELECT * FROM test_table;
 a
---
 1
 2
(2 rows)


Оптимизация

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

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

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

Текущее состояние пула автономных сеансов можно просмотреть при помощи специально разработанной функции autonomous_pool_show().

SELECT * FROM autonomous_pool_show();

Пример возможного вывода функции autonomous_pool_show() для пула размером 5:

 slot | state  | worker | owner | owner_type | database | auth_user | cnt
------+--------+--------+-------+------------+----------+-----------+-----
    0 | Idle   |   1863 |     0 |            |        5 |     16385 |   2
    1 |        |        |       |            |          |           |
    2 | Active |   1880 |  1865 | Backend    |       12 |        10 |   1
    3 |        |        |       |            |          |           |
    4 | Idle   |   1872 |     0 |            |        5 |        10 |   3
(5 rows)

Слоты номер 1 и 3 свободны, что отображается как NULL во всех столбцах.

Столбцы database и auth_user имеют отношение к совместимости обслуживающего процесса и автономного сеанса. Новый обслуживающий процесс (обычный сеанс) может повторно использовать только те автономные сеансы, которые имеют идентичное подключение.

Последний столбец cnt содержит текущий счетчик использований автономного сеанса (см. параметр конфигурации autonomous_session_max_uses ниже).



Конфигурирование

Таблица 1. Параметры конфигурации

ИмяЗначение по умолчаниюКонтекст GUC
autonomous_pool_capacity0qhbmaster
autonomous_session_max_uses10sighup

Пул автономных сеансов размещается в области общей памяти; размер пула ограничен значением параметра autonomous_pool_capacity (по умолчанию 0, что означает недоступность автономных транзакций). После изменения значения этого параметра необходимо перезапустить QHB. Значение этого параметра не может превышать значение параметра max_worker_processes; при нарушении этого условия возникнет фатальная ошибка после запуска QHB. Поскольку фоновые рабочие процессы используются и для других целей, на практике значение параметра max_worker_processes должно превышать значение autonomous_pool_capacity с некоторым «запасом». При увеличении емкости пула автономных сеансов необходимо соответственно увеличить значение параметра max_worker_processes.

Время жизни автономного сеанса приходится ограничивать из-за накопления кеша (требующего много оперативной памяти) при выполнении SQL-запросов в рабочем процессе. Для этого предусмотрен параметр autonomous_session_max_uses (значение по умолчанию 10, которое можно менять без перезапуска QHB). При превышении количества использований автономный сеанс уничтожается, освобождая ресурсы.



Ограничения

  • Количество одновременно активных автономных сеансов ограничено значением параметра autonomous_pool_capacity.

  • Автономная транзакция может присоединиться только к той же базе, к которой присоединен родительский сеанс.

  • Автономная транзакция синхронная: вызывающая транзакция всегда ожидает завершения автономной.

  • Уровень изоляции автономной транзакции переопределить нельзя, он наследуется от вызывающей транзакции.

  • Вызов процедуры (CALL) внутри автономной транзакции запрещен из-за того, что выполнение COMMIT или ROLLBACK в процедуре (запрещенное в функции) может конфликтовать с активной автономной транзакцией, а убедиться в отсутствии конфликта в общем случае невозможно.

  • Параллельные запросы не поддерживаются.

  • Курсоры не поддерживаются.

  • Подготовленные операции не поддерживаются.

  • Динамический SQL с заполнителями для параметров не поддерживается.

  • Ошибки и уведомления передаются в родительский сеанс и повторно выбрасываются.

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



Примеры

Результаты работы примеров приведены для конфигурации с разрешенными автономными транзакциями:

echo "autonomous_pool_capacity = 5" >> $PGDATA/qhb.conf
echo "autonomous_session_max_uses = 100" >> $PGDATA/qhb.conf
qhb_ctl restart

Пример использования автономных транзакций для аудита или отладки действий

Предположим, что у нас есть таблица titles, операции с данными в которой должны автоматически фиксироваться в специальной таблице-журнале log.

CREATE TABLE titles (id INT, title_name VARCHAR);
CREATE TABLE log (moment TIMESTAMPTZ, user_name VARCHAR, action VARCHAR);

Определим функцию для записи в log в автономной транзакции.

CREATE OR REPLACE FUNCTION log_insert() RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
    action VARCHAR;
BEGIN AUTONOMOUS
    IF (TG_OP = 'INSERT') THEN
	action := 'Added id=' || NEW.id::text;
    ELSIF (TG_OP = 'UPDATE') THEN
	action := 'Updated id=' || NEW.id::text;
    ELSIF (TG_OP = 'DELETE') THEN
	action := 'Deleted id=' || OLD.id::text;
    END IF;
    INSERT INTO log VALUES (now(), current_user, action);
    RETURN NEW;
END;
$$;

Обеспечим выполнение этой функции в триггере.

CREATE OR REPLACE TRIGGER log_trigger
AFTER INSERT OR UPDATE OR DELETE
ON titles
FOR EACH ROW
EXECUTE PROCEDURE log_insert();

Выполним несколько операций с таблицей titles и откатим транзакцию.

START TRANSACTION;
INSERT INTO titles VALUES (8001, 'First');
INSERT INTO titles VALUES (8002, 'Second');
INSERT INTO titles VALUES (8003, 'Third');
DELETE FROM titles WHERE id = 8001;
ROLLBACK;
SELECT * FROM titles;
  id  | title_name
------+------------
(0 rows)

Удостоверимся, что информация об операциях сохранилась, несмотря на откат.

SELECT * FROM log;
	    moment              | user_name |     action
-------------------------------+-----------+-----------------
 2025-03-07 19:22:45.32366+03  | qhb       | Added id=8001
 2025-03-07 19:22:45.326015+03 | qhb       | Added id=8002
 2025-03-07 19:22:45.326327+03 | qhb       | Added id=8003
 2025-03-07 19:22:45.326749+03 | qhb       | Deleted id=8001
(3 rows)

Если бы в тексте функции триггера отсутствовало ключевое слово AUTONOMOUS, таблица log тоже оказалась бы пуста.


Пример использования автономных транзакций для упрощения логики

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

Смоделируем элементарную операцию вставкой строки данных из таблицы work в таблицу backup.

CREATE TABLE work (id INT, name VARCHAR);
INSERT INTO work VALUES (1, 'First');
INSERT INTO work VALUES (2, 'Second');
INSERT INTO work VALUES (3, 'Third');
INSERT INTO work VALUES (4, 'Fourth');
INSERT INTO work VALUES (5, 'Fifth');
INSERT INTO work VALUES (6, 'Sixth');
INSERT INTO work VALUES (7, 'Seventh');
INSERT INTO work VALUES (8, 'Eighth');
INSERT INTO work VALUES (9, 'Ninth');
INSERT INTO work VALUES (10, 'Tenth');
CREATE TABLE backup (id INT, name VARCHAR);

Пусть на каждом нечетном идентификаторе после 2 происходит сбой.

CREATE OR REPLACE FUNCTION process_row(id INT, name VARCHAR) RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN AUTONOMOUS
    IF id = 1 OR id % 2 = 0 THEN
	INSERT INTO backup VALUES(id, name);
    ELSE
	SELECT 1/0;
    END IF;
END;
$$;

При выполнении основной транзакции будем игнорировать исключения.

CREATE OR REPLACE FUNCTION do_backup() RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN SELECT id, name FROM work
    LOOP
        BEGIN
            PERFORM process_row(r.id, r.name);
	EXCEPTION WHEN OTHERS
	THEN
	END;
    END LOOP;
END;
$$;

Запускаем основную транзакцию.

START TRANSACTION;
SELECT do_backup();
COMMIT;

И убеждаемся, что «несбойные» данные скопировались.

SELECT * FROM backup;
 id |  name  
----+--------
  1 | First
  2 | Second
  4 | Fourth
  6 | Sixth
  8 | Eighth
 10 | Tenth
(6 rows)

Здесь применение автономных транзакций помогло упростить программный код приложения.

Если убрать AUTONOMOUS из текста функции process_row, то таблица backup после выполнения do_backup окажется пуста.