Stored procedures в SQL: полный гайд
Что такое хранимая процедура
Хранимая процедура (stored procedure) — это именованный блок SQL-кода с возможной логикой, переменными и циклами, который живёт на сервере базы данных. В отличие от запросов, которые вы каждый раз отправляете из клиента, процедура хранится в БД и вызывается по имени.
На практике это выглядит так: вместо того чтобы каждый раз писать «удали старые записи, пересчитай метрики, залей в витрину» тремя отдельными SQL-запросами, вы оформляете всё это одной процедурой refresh_daily_mart() и вызываете её одной строкой CALL refresh_daily_mart();. Логика живёт в БД, клиент только дергает её.
Для аналитика процедуры важны по двум причинам. Первая — часто именно аналитик пишет или читает код процедур, особенно на позициях DA+DE или в банках. Вторая — понимание того, как работают stored procedures, помогает общаться с дата-инженерами и не переизобретать велосипед.
Чем процедура отличается от функции
Это часто путают. Разница на уровне SQL-стандарта:
Функция возвращает значение (скаляр или таблицу). Её можно вызвать внутри SELECT: SELECT calculate_retention(user_id) FROM users;. Функции обычно без side-effects — не меняют данные.
Процедура ничего не возвращает (или возвращает через OUT-параметры). Её нельзя вызвать внутри SELECT — только через CALL procedure_name() или EXEC. Процедуры могут менять данные: делать UPDATE, DELETE, INSERT, управлять транзакциями.
Простое правило: нужно вычислить значение — функция. Нужно выполнить последовательность действий — процедура.
В PostgreSQL до 11-й версии процедур в полном смысле не было, всё оформлялось функциями. С версии 11 появился CREATE PROCEDURE с возможностью управлять транзакциями. В MySQL и SQL Server процедуры есть давно и в полноценном виде.
Синтаксис в PostgreSQL
Базовый пример:
CREATE OR REPLACE PROCEDURE archive_old_orders(cutoff_date DATE)
LANGUAGE plpgsql
AS $$
BEGIN
-- Переносим старые заказы в архив
INSERT INTO orders_archive
SELECT * FROM orders WHERE created_at < cutoff_date;
-- Удаляем из основной таблицы
DELETE FROM orders WHERE created_at < cutoff_date;
-- Коммитим промежуточно, если нужно
COMMIT;
RAISE NOTICE 'Archived orders before %', cutoff_date;
END;
$$;
-- Вызов
CALL archive_old_orders('2024-01-01');Язык процедур PostgreSQL — это PL/pgSQL, расширение SQL с переменными, условиями, циклами. Выглядит как урезанный Python внутри Oracle-подобного синтаксиса.
Переменные, условия, циклы
Стандартные элементы процедурного программирования:
CREATE OR REPLACE PROCEDURE update_user_segments()
LANGUAGE plpgsql AS $$
DECLARE
user_rec RECORD;
segment_name TEXT;
BEGIN
FOR user_rec IN SELECT user_id, total_spent FROM users LOOP
IF user_rec.total_spent > 100000 THEN
segment_name := 'vip';
ELSIF user_rec.total_spent > 10000 THEN
segment_name := 'regular';
ELSE
segment_name := 'casual';
END IF;
UPDATE users SET segment = segment_name WHERE user_id = user_rec.user_id;
END LOOP;
END;
$$;Это цикл по таблице с условной логикой. На практике такой код пишется редко — обычно то же самое делается одним UPDATE с CASE WHEN, что быстрее в 10-100 раз. Но бывают случаи, где построчная обработка оправдана: сложная логика с вызовами других процедур, внешние API, транзакционные границы.
Когда процедуры уместны
Не каждая задача просит процедуру. Критерии, когда они оправданы:
Повторяющаяся логика из нескольких шагов. Если вы регулярно делаете цепочку INSERT → UPDATE → DELETE → INSERT, это просится в процедуру. Поддерживать один блок проще, чем четыре разных скрипта в Airflow.
Инкапсуляция транзакций. Нужно, чтобы несколько действий прошли атомарно. Процедура с BEGIN/COMMIT даёт гарантии, что либо всё выполнилось, либо ничего.
Права доступа. Пользователю можно дать право вызывать процедуру, но не давать прямой доступ к таблицам. Это полезно для приложений, где нельзя раскрывать внутреннюю схему.
Предварительные вычисления. Сложная агрегация, которую дорого делать на каждом запросе. Процедура считает один раз за ночь, кладёт в материализованную таблицу.
Разобраться с такими штуками на уровне собеседования удобно через практику — в тренажёре Карьерник есть задачи на SQL разного уровня с разборами, в том числе по DDL и процедурным расширениям.
Когда процедуры — плохая идея
Обратная сторона — когда процедуры усложняют жизнь:
Бизнес-логика приложения. Если правила «как считать выручку» живут в процедурах, это значит, что логика распылена между приложением и БД. Отладка становится кошмаром. Обычно лучше держать бизнес-логику в коде на Python/Java, а БД использовать только как хранилище.
Сложные вычисления. То, что в Python делается за 10 строк pandas, в PL/pgSQL займёт 50 строк и будет медленнее. БД хороша для SET-операций, не для процедурного программирования.
Переносимость. Процедуры в PostgreSQL написаны на PL/pgSQL, в MySQL на MySQL-specific синтаксисе, в SQL Server на T-SQL. При миграции между СУБД всю логику придётся переписывать. Функции и VIEW переносятся проще.
Процедуры vs приложение
Вечный спор: логика в БД или в приложении? Короткий ответ — зависит.
В банках и enterprise процедуры используются массово. Исторически БД была главным местом логики, приложения были тонкими клиентами. Сейчас это меняется, но legacy-системы на миллионы строк PL/SQL никуда не денутся.
В современных IT-продуктах процедур обычно мало. Вся логика в микросервисах, БД — хранилище. ORM (SQLAlchemy, Hibernate) генерирует простые запросы, сложная обработка идёт в коде.
В аналитических пайплайнах используют сочетание: простые трансформации в dbt-моделях (которые по сути — VIEW/TABLE с SQL), периодические задачи — процедуры или функции, запускаемые из Airflow.
Пример из жизни: ежедневная витрина
Типовая задача — ежедневно обновлять агрегированную таблицу с метриками:
CREATE OR REPLACE PROCEDURE refresh_daily_metrics(target_date DATE)
LANGUAGE plpgsql AS $$
BEGIN
-- Удаляем данные за эту дату, если они уже есть
DELETE FROM mart_daily_metrics WHERE day = target_date;
-- Вставляем свежий расчёт
INSERT INTO mart_daily_metrics (day, dau, revenue, new_users)
SELECT
target_date AS day,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'app_open') AS dau,
SUM(amount) FILTER (WHERE event_name = 'purchase') AS revenue,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'signup') AS new_users
FROM events
WHERE event_time::DATE = target_date;
-- Логируем
INSERT INTO refresh_log (table_name, target_date, refreshed_at)
VALUES ('mart_daily_metrics', target_date, NOW());
END;
$$;
-- Вызов из Airflow DAG
CALL refresh_daily_metrics(CURRENT_DATE - 1);Процедура атомарно обновляет витрину и пишет лог. Если что-то сломалось — всё откатится, старые данные останутся.
Ошибки новичков
Первая — писать всю аналитическую логику в процедурах. Так делали в 2000-х, сейчас это антипаттерн. Простые SELECT с CTE и оконными функциями обычно понятнее и быстрее.
Вторая — забыть про транзакции. Процедура делает DELETE, потом INSERT, потом падает на строчке UPDATE. Без явной транзакции часть данных удалилась, часть вставилась — витрина сломана. Всегда оборачивайте в BEGIN/COMMIT или используйте SAVEPOINT.
Третья — не логировать ошибки. Когда процедура падает ночью, без логов непонятно, в какой строке. Используйте RAISE NOTICE для информации и RAISE EXCEPTION для критических ошибок.
Четвёртая — создавать процедуры, дублирующие функциональность. Если у вас три разные процедуры для обновления трёх похожих витрин, вынесите общий код в отдельную процедуру, которая вызывается из трёх.
На собеседовании
Junior-аналитика про процедуры обычно не спрашивают. Middle+ — могут, особенно в банках и enterprise. Типичные вопросы:
Чем процедура отличается от функции? Функция возвращает значение, процедура — нет. Функцию можно вызвать в SELECT, процедуру — только через CALL.
Зачем процедуры вообще нужны? Инкапсуляция сложной логики, атомарные транзакции, контроль доступа.
Почему бизнес-логика в процедурах — плохая идея? Распыление логики между приложением и БД, сложность отладки и тестирования, vendor lock-in.
Какой язык используется? В PostgreSQL — PL/pgSQL. В SQL Server — T-SQL. В Oracle — PL/SQL. В MySQL — свой процедурный диалект. Все похожи, но не совместимы.
Читайте также
FAQ
Процедура быстрее обычного SQL?
Не обязательно. SQL внутри процедуры выполняется тем же оптимизатором. Выигрыш в том, что клиенту не нужно слать несколько запросов по сети — всё в одном вызове. На низкой latency-связи это экономит время.
Можно ли тестировать процедуры?
Можно, но неудобно. Обычно через pgTAP для PostgreSQL или tSQLt для SQL Server. Это ещё одна причина, почему сложную логику лучше держать в приложении — там нормальные unit-тесты.
Что такое триггер и как он связан с процедурой?
Триггер — это процедура, которая вызывается автоматически при INSERT/UPDATE/DELETE на таблице. Удобно для аудита (логировать изменения) или денормализации (поддерживать счётчики). На практике используйте осторожно: неявные вызовы сложно отлаживать.
dbt заменяет процедуры?
Частично. dbt хорош для трансформаций в формате SELECT → INSERT. Процедурная логика (циклы, условия, изменение данных по строкам) остаётся за процедурами или Python-скриптами в Airflow.