Ошибки в PostgreSQL могут возникать по множеству причин: нарушение ограничений (NOT NULL, UNIQUE, CHECK), ошибки синтаксиса, дублирующиеся значения и т.д. Если не перехватывать и не обрабатывать такие ошибки, вся внешняя транзакция может быть полностью откатана. Для устойчивых бизнес-операций важно правильно их обрабатывать.
PL/pgSQL предоставляет мощный механизм блоков BEGIN ... EXCEPTION ... END для перехвата и обработки ошибок в функциях и процедурах. Они похожи на try-catch из Python или Java, но с особенностями работы в контексте транзакций PostgreSQL.
Важный момент:
Каждый блок BEGIN ... EXCEPTION ... END работает как “виртуальный savepoint”. Если возникает исключение — все изменения этого блока автоматически откатываются. Это единственный корректный способ частичного отката в функциях и процедурах на PL/pgSQL. ВАЖНО: это не бесплатно — каждый BEGIN с обработчиком EXCEPTION создаёт subtransaction, что заметно дороже обычного блока без EXCEPTION. Не оборачивайте каждый INSERT в EXCEPTION «на всякий случай» — используйте только там, где действительно нужен частичный откат.
Синтаксис обработки ошибок с помощью EXCEPTION
BEGIN
-- основной код
EXCEPTION
WHEN ТИП_ОШИБКИ THEN
-- обработка конкретной ошибки
WHEN ДРУГОЙ_ТИП_ОШИБКИ THEN
-- другой обработчик
WHEN OTHERS THEN
-- обработка всех прочих ошибок
END;
Иллюстрация в функциях/процедурах
DO $$
BEGIN
RAISE NOTICE 'Сейчас будет ошибка...';
PERFORM 1 / 0;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Деление на ноль перехвачено!';
END;
$$;
Пример: обновление с обработкой и откатом ошибок
Пусть есть таблица заказов:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL,
status TEXT NOT NULL
);
Создадим функцию, которая обновляет статус заказа и при ошибке ничего не меняет:
CREATE OR REPLACE FUNCTION update_order_status(order_id INT, new_status TEXT)
RETURNS VOID AS $$
BEGIN
BEGIN
UPDATE orders
SET status = new_status
WHERE id = order_id;
-- Симуляция ошибки
IF new_status = 'FAIL' THEN
RAISE EXCEPTION 'Симулируем ошибку!';
END IF;
RAISE NOTICE 'Статус заказа % обновлён', order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Ошибка при обновлении заказа %: %', order_id, SQLERRM;
-- Все изменения внутри блока автоматически откатятся!
-- Повторно выбрасываем ошибку для внешней обработки, если нужно
RAISE;
END;
END;
$$ LANGUAGE plpgsql;
Как это работает в процедурах с явным управлением транзакциями
В процедурах (CREATE PROCEDURE) можно использовать COMMIT и ROLLBACK, но нельзя писать SAVEPOINT, RELEASE SAVEPOINT и ROLLBACK TO SAVEPOINT — все три команды в PL/pgSQL запрещены. Если нужно откатить только часть операций внутри процедуры, используйте всё тот же BEGIN ... EXCEPTION ... END:
CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
-- Вся процедура может использовать COMMIT/ROLLBACK, но для отката отдельного этапа:
BEGIN
UPDATE accounts
SET balance = balance - amount
WHERE id = (SELECT account_id FROM orders WHERE id = order_id);
-- ошибка
IF amount < 0 THEN
RAISE EXCEPTION 'Сумма не может быть отрицательной!';
END IF;
UPDATE orders SET status = 'PAID' WHERE id = order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Ошибка при обработке оплаты заказа %: %', order_id, SQLERRM;
-- изменения в этом блоке откатятся автоматически
END;
COMMIT; -- Можно явно завершать транзакцию только в процедурах!
END;
$$;
Логирование ошибок
Важно не просто перехватывать ошибки, но и сохранять их для дальнейшего анализа.
CREATE TABLE error_log (
id SERIAL PRIMARY KEY,
order_id INT,
error_message TEXT,
error_time TIMESTAMP DEFAULT now()
);
В функции или процедуре:
EXCEPTION
WHEN OTHERS THEN
INSERT INTO error_log (order_id, error_message)
VALUES (order_id, SQLERRM);
RAISE NOTICE 'Ошибка занесена в журнал: %', SQLERRM;
RAISE;
Важные ограничения и нюансы в PostgreSQL 17
Внутри функций (CREATE FUNCTION ... ) команды транзакционного управления (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT) использовать нельзя! Все функции выполняются целиком в рамках внешней транзакции.
Внутри процедур (CREATE PROCEDURE ... ) можно явно писать COMMIT и ROLLBACK. НО: SAVEPOINT, RELEASE SAVEPOINT и ROLLBACK TO SAVEPOINT — ЗАПРЕЩЕНЫ! (Вы получите ошибку синтаксиса, если попытаетесь использовать любую из них в PL/pgSQL).
Откат "части кода" внутри функций и процедур делается при помощи блоков BEGIN ... EXCEPTION ... END. При возникновении ошибки всё внутри блока откатывается автоматически, и исполнение может пойти дальше.
Процедуры (CREATE PROCEDURE) нельзя запускать внутри функции или через SELECT — только отдельной командой CALL ....
Как реально реализовать "частичный откат" в PL/pgSQL?
Единственный рабочий способ — использовать обработку ошибок через блоки BEGIN ... EXCEPTION ... END. Такой блок автоматически создаёт точку сохранения, и при ошибке откатывает изменения внутри блока, не затрагивая остальную часть процедуры/функции.
Пример с EXCEPTION (рекомендуемый подход):
CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
-- Какой-то код
BEGIN
-- Здесь ошибка не приведёт к откату всей процедуры,
-- только этого блока!
INSERT INTO demo VALUES ('bad data'); -- возможно, вызовет ошибку
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Ошибка обработана, изменения внутри блока отменены';
END;
-- Здесь выполнение продолжается!
END;
$$;
Пример: загрузка пакета данных с защитой от отката всего процесса
CREATE OR REPLACE PROCEDURE load_big_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM import_table LOOP
BEGIN
INSERT INTO target_table (col1, col2)
VALUES (rec.col1, rec.col2);
EXCEPTION WHEN OTHERS THEN
INSERT INTO import_errors (err_msg)
VALUES ('Ошибка в записи: ' || rec.col1 || ': ' || SQLERRM);
-- изменения внутри этого блока отменены!
END;
END LOOP;
COMMIT; -- допустимо только если процедура запущена вне явной внешней транзакции!
END;
$$;
-- Вызов процедуры
CALL load_big_batch();
Обратите внимание: если вызвать такую процедуру из клиента, у которого уже начата транзакция (например, Python с autocommit=False), выполнение COMMIT или SAVEPOINT внутри процедуры вызовет ошибку.
Советы по работе с вложенными точками сохранения и EXCEPTION
- Не используйте SAVEPOINT, RELEASE SAVEPOINT и ROLLBACK TO SAVEPOINT в PL/pgSQL! Все три команды запрещены и вызовут синтаксическую ошибку.
- Для частичного отката всегда используйте вложенные блоки BEGIN ... EXCEPTION ... END.
- Не забывайте, что COMMIT и ROLLBACK внутри процедур перезапускают транзакцию — их можно вызывать только когда процедура запущена в autocommit-режиме!
- Логируйте ошибки в отдельную таблицу, чтобы не терять информацию о некорректных строках.
- Если бизнес-операция должна быть строго атомарной (всё-или-ничего) — делайте её функцией, без COMMIT/ROLLBACK внутри; если нужна обработка поэтапно — реализуйте как процедуру.
Пример: импорт по пакетам с частичной обработкой
CREATE OR REPLACE PROCEDURE import_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM staging_table LOOP
BEGIN
INSERT INTO data_table (data)
VALUES (rec.data);
EXCEPTION
WHEN unique_violation THEN
INSERT INTO import_log (msg)
VALUES ('Дубликат: ' || rec.data);
WHEN OTHERS THEN
INSERT INTO import_log (msg)
VALUES ('Ошибка: ' || rec.data || ' — ' || SQLERRM);
END;
END LOOP;
END;
$$;
Главное, что нужно запомнить для PostgreSQL 17:
В PL/pgSQL процедурах разрешены COMMIT и ROLLBACK, а SAVEPOINT, RELEASE SAVEPOINT и ROLLBACK TO SAVEPOINT — нельзя (все три запрещены полностью).
"Частичный откат" внутри функций и процедур реализуется только с помощью вложенных блоков BEGIN ... EXCEPTION ... END.
Транзакциями лучше управлять снаружи (через параметры подключения и autocommit), а внутри процедур — использовать описанные механизмы обработки ошибок.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ