Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи?

Написать
Войти
Дайджесты
Слон, символ PostgreSQL, подсвеченный лазерными лучами оптимизации, рядом с открытыми вкладками с кодом SQL-запросов

10 скрытых возможностей PostgreSQL для оптимизации backend-архитектуры

Современный PostgreSQL умеет решать множество задач, которые разработчики привыкли перекладывать на внешние сервисы и прикладной код. Разбираем 10 недооцененных фич базы данных (SKIP LOCKED, оконные функции, Advisory locks, generated columns и др.), позволяющих упростить архитектуру backend-приложений.

10 скрытых возможностей PostgreSQL для оптимизации backend-архитектуры

В современной разработке бэкенда существует устойчивая тенденция: при возникновении любой новой задачи внедрять в стек специализированный инструмент. Нужна простая очередь задач? Ставим Redis и Celery. Нужен поиск по статьям? Разворачиваем кластер Elasticsearch. Нужны распределенные блокировки? Снова пишем интеграцию с Redis или Consul.

Такой подход оправдан при огромных масштабах и специфических требованиях, однако для большинства приложений средней и умеренной нагрузки он лишь усложняет инфраструктуру, увеличивает сетевые задержки и повышает стоимость поддержки. Реляционная база данных PostgreSQL является мощнейшим комбайном, способным в одиночку закрыть большинство этих задач транзакционно, надежно и с минимальными затратами. Разберем 10 недооцененных возможностей PostgreSQL, о которых должен знать каждый бэкенд-разработчик.

1. Очередь задач через FOR UPDATE SKIP LOCKED

Часто для обработки фоновых задач (например, отправки писем или генерации документов) создается таблица-очередь. Однако при параллельной работе нескольких воркеров возникает проблема конкуренции: как сделать так, чтобы воркеры не хватали одну и ту же задачу одновременно?

Начиная с версии 9.5 в PostgreSQL доступна конструкция FOR UPDATE SKIP LOCKED. Она позволяет заблокировать выбранную строку для текущего воркера, а остальные параллельные процессы просто проигнорируют заблокированные строки и возьмут следующую свободную запись.

-- Пример выборки задачи воркером
BEGIN;
SELECT id, payload 
FROM jobs 
WHERE status = 'pending' AND run_at <= now()
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;

-- Обновление статуса выбранной задачи
UPDATE jobs SET status = 'processing', locked_at = now() WHERE id = :id;
COMMIT;

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

2. Оконные функции для аналитики на сервере БД

Часто разработчики выгружают массив данных в код приложения (на Python, Go или Node.js) и начинают выполнять вычисления в циклах: группировать, сортировать, искать разницу во времени между событиями. Это неэффективно расходует оперативную память и процессорное время серверов приложений.

Оконные функции позволяют делать аналитические расчеты прямо в SQL. Например, используя функцию LAG(), можно легко вычислить время, прошедшее между последовательными изменениями статусов заказа:

SELECT order_id, status, created_at,
       created_at - LAG(created_at) OVER(PARTITION BY order_id ORDER BY created_at) as duration
FROM order_events;

3. DISTINCT ON для выборки последних состояний

Когда нужно получить последнее событие по каждому заказу или свежую сессию пользователя, стандартный SQL заставляет писать сложные подзапросы с группировками (GROUP BY, MAX(created_at)) и последующим обратным объединением (JOIN).

Конструкция DISTINCT ON — это уникальная фича PostgreSQL, которая решает эту задачу в один шаг. Она гарантирует возвращение ровно одной строки для каждого уникального ключа по заданному правилу сортировки:

-- Получить самую последнюю сессию для каждого пользователя
SELECT DISTINCT ON (user_id) user_id, ip, created_at
FROM user_sessions
ORDER BY user_id, created_at DESC;

4. Вычисляемые поля без триггеров: Generated Columns

Появившиеся в PostgreSQL 12 вычисляемые столбцы (GENERATED ALWAYS AS ... STORED) вычисляют свое значение автоматически при вставке или обновлении строки. Они полезны для хранения нормализованных данных (например, email в нижнем регистре без пробелов для уникального индекса) или вычисления сумм строк заказа:

CREATE TABLE app_users (
    id bigserial PRIMARY KEY,
    email text NOT NULL,
    normalized_email text GENERATED ALWAYS AS (lower(trim(email))) STORED
);

5. Особенности MVCC и настройка Autovacuum

База данных PostgreSQL использует механизм многоверсионности (MVCC). При обновлении (UPDATE) или удалении (DELETE) строки старая версия физически остается в таблице как «мертвый кортеж» (dead tuple). Очисткой этих мертвых строк занимается фоновый процесс autovacuum.

Если вы активно используете таблицы в качестве очередей задач (с частыми UPDATE и DELETE), autovacuum по умолчанию может не справляться, что приведет к раздуванию (bloat) таблиц и индексов и падению производительности. Для таких горячих таблиц необходимо настраивать параметры очистки индивидуально:

ALTER TABLE jobs SET (
    autovacuum_vacuum_scale_factor = 0.05,
    autovacuum_vacuum_threshold = 100
);

6. LATERAL JOIN как циклы foreach в SQL

Конструкция LATERAL JOIN позволяет правому подзапросу ссылаться на колонки строк из левой таблицы. Это эквивалентно циклу foreach в императивном программировании. Типичный сценарий — выбрать топ-N связанных записей для каждой строки основной таблицы (например, три последних заказа для каждого клиента):

SELECT c.name, o.id, o.created_at
FROM customers c
LEFT JOIN LATERAL (
    SELECT id, created_at 
    FROM orders 
    WHERE customer_id = c.id 
    ORDER BY created_at DESC 
    LIMIT 3
) o ON true;

7. Рекурсивные запросы (Recursive CTE)

Для хранения иерархических структур (меню, категории товаров, комментарии и их ветки ответов) разработчики часто делают несколько последовательных запросов в базу или реализуют рекурсию в коде приложения.

С помощью WITH RECURSIVE можно обойти все дерево категорий любой вложенности за один SQL-запрос, сформировав полный путь к каждому элементу:

WITH RECURSIVE category_tree AS (
    SELECT id, parent_id, name, name::text AS path
    FROM categories
    WHERE parent_id IS NULL
  UNION ALL
    SELECT c.id, c.parent_id, c.name, t.path || ' -> ' || c.name
    FROM categories c
    JOIN category_tree t ON c.parent_id = t.id
)
SELECT * FROM category_tree;

8. Распределенные блокировки через Advisory Locks

Вместо поднятия Redis для синхронизации cron-задач (например, чтобы отчет не генерировался двумя инстансами одновременно), можно использовать встроенные в PostgreSQL рекомендательные блокировки (Advisory Locks). Они не привязаны к таблицам или строкам и управляются на уровне сессии или транзакции:

-- Попытка получить блокировку по хэшу имени задачи
SELECT pg_try_advisory_lock(hashtext('daily_report')) AS acquired;
-- Возвращает true, если блокировка успешно получена

Обязательно освобождайте блокировку после завершения работы с помощью pg_advisory_unlock().

9. Встроенный полнотекстовый поиск (FTS)

Для простого текстового поиска по каталогу товаров, статьям или FAQ не обязательно разворачивать Elasticsearch. PostgreSQL из коробки поддерживает словари, стемминг (включая русский язык) и специальные GIN-индексы для мгновенного поиска:

-- Пример поиска с использованием весов для релевантности
SELECT title, ts_rank(to_tsvector('russian', title || ' ' || body), query) as rank
FROM articles, to_tsquery('russian', 'базы & оптимизация') query
WHERE to_tsvector('russian', title || ' ' || body) @@ query
ORDER BY rank DESC;

10. Частичные индексы (Partial Indexes)

Если ваши запросы обычно ищут только активные записи (например, неоплаченные счета или неудаленные пользователи deleted_at IS NULL), нет смысла индексировать миллионы архивных строк. Частичный индекс строится только по строкам, удовлетворяющим условию WHERE, что экономит дисковое пространство и ускоряет запись:

CREATE INDEX idx_active_orders 
ON shop_orders (user_id) 
WHERE status = 'pending';

Заключение

Использование встроенных фич PostgreSQL позволяет значительно сократить объем прикладного кода, избавиться от лишних инфраструктурных сервисов и гарантировать целостность данных на уровне СУБД. Начните проектировать базу данных как полноценную прикладную платформу, а не просто как глупое хранилище таблиц.