Помилки в 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 всередині процедури викличе помилку.
Поради щодо роботи з вкладеними точками збереження і 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), а всередині процедур — використовувати описані механізми обробки помилок.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ