Ошибки в PostgreSQL могут возникать по множеству причин: нарушение ограничений (NOT NULL, UNIQUE, CHECK), ошибки синтаксиса, дублирующиеся значения и т.д. Если не перехватывать и не обрабатывать такие ошибки, вся внешняя транзакция может быть полностью откатана. Для устойчивых бизнес-операций важно правильно их обрабатывать.
PL/pgSQL предоставляет мощный механизм блоков BEGIN ... EXCEPTION ... END для перехвата и обработки ошибок в функциях и процедурах. Они похожи на try-catch из Python или Java, но с особенностями работы в контексте транзакций PostgreSQL.
Важный момент:
Каждый блок BEGIN ... EXCEPTION ... END работает как “виртуальный savepoint”. Если возникает исключение — все изменения этого блока автоматически откатываются. Это единственный корректный способ частичного отката в функциях и процедурах на PL/pgSQL
Синтаксис обработки ошибок с помощью 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, но нельзя делать ROLLBACK TO SAVEPOINT. Если нужно откатить только часть операций внутри процедуры, используйте всё тот же 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 ... ) можно явно писать SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK. НО: ROLLBACK TO 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
- Не используйте 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 процедурах разрешены SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK, но ROLLBACK TO SAVEPOINT — нельзя.
"Частичный откат" внутри функций и процедур реализуется только с помощью вложенных блоков BEGIN ... EXCEPTION ... END.
Транзакциями лучше управлять снаружи (через параметры подключения и autocommit), а внутри процедур — использовать описанные механизмы обработки ошибок.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ