Таблица на сотни миллионов строк рано или поздно начинает мешать: autovacuum не успевает, индексы распухают, а удаление старых данных растягивается на часы. Партиционирование не ускоряет всё подряд, но делает работу предсказуемой: старые данные уходят куском, новые пишутся в свежую партицию, а запросы с фильтром по дате читают только нужные файлы. Разберём декларативный синтаксис PostgreSQL, обслуживание и подводные камни.

Когда партиционирование помогает, а когда нет

Партиции имеют смысл при больших объёмах и естественном ключе деления: время, регион, тип события. Типичный пример — журнал событий, где 95 процентов запросов смотрят последние сутки, а старые данные хранятся по требованиям аудита. Второй пример — таблица заказов, которую удобно архивировать по месяцам.

Если таблица занимает пару гигабайт и запросы идут по индексу, партиционирование добавит сложности без выигрыша. Помните и об ограничениях: уникальный индекс должен включать ключ партиционирования, а глобальных индексов через все партиции в PostgreSQL нет. Запрос без фильтра по ключу обойдёт все партиции и может работать медленнее исходной таблицы.

Базовая схема с диапазонами дат

Декларативное партиционирование доступно с PostgreSQL 10, hash-партиции и внешние ключи появились позже, а identity-колонки в партициях поддерживаются с PostgreSQL 17. Синтаксис един: родительская таблица объявляет метод и ключ, дочерние описывают границы.

CREATE TABLE events (
    id         bigint GENERATED ALWAYS AS IDENTITY,
    created_at timestamptz NOT NULL,
    source     text NOT NULL,
    payload    jsonb NOT NULL,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2026_01 PARTITION OF events
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

CREATE TABLE events_2026_02 PARTITION OF events
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

Границы задаются в формате «от включая, до не включая». Это удобно: конец января и начало февраля не пересекаются, а дата 1 февраля попадёт ровно в одну партицию. Ключ timestamptz храните в UTC, иначе переход на летнее время создаст сюрпризы на границах месяцев.

Первичный ключ обязан включать ключ партиционирования, поэтому в примере он составной: id и created_at. Уникальный индекс по колонке вне ключа создать нельзя.

Создание партиций заранее

Главная эксплуатационная обязанность — заранее создавать следующую партицию. Если её нет, вставка завершится ошибкой «no partition of relation found for row». Автоматизируйте процесс через pg_partman или собственный скрипт с блоком DO, который вызывается по cron или systemd-таймеру.

DO $$
DECLARE
    start_date date := date_trunc('month', now() + interval '1 month')::date;
    end_date   date := (start_date + interval '1 month')::date;
    part_name  text := 'events_' || to_char(start_date, 'YYYY_MM');
BEGIN
    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS %I PARTITION OF events FOR VALUES FROM (%L) TO (%L)',
        part_name, start_date, end_date);
END $$;

Скрипт идемпотентен благодаря IF NOT EXISTS. Запускайте его за несколько дней до границы, а не в последнюю минуту: создание партиции берёт короткую блокировку на родителя, и в час пик это может стать заметно. Держите запас на два-три месяца вперёд, чтобы отпуск или сбой таймера не остановил запись.

Проверка отсечения партиций

Отсечение, или pruning, — главный источник выигрыша. Планировщик должен видеть в запросе ограничение по ключу. Сравните планы с фильтром и без него: в первом случае в строке Append будет указано число выбранных партиций, во втором — все.

EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM events
WHERE created_at >= now() - interval '1 day';

EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM events
WHERE payload ->> 'type' = 'login';

Во втором запросе ключа партиционирования нет, поэтому PostgreSQL честно просканирует всё. Если такие запросы частые, подумайте о другом ключе или о денормализации: вынесите тип в отдельную колонку и добавьте её в ключ. Отсечение работает и для prepared statements: при выполнении значения подставляются и план уточняется, хотя при неудачном выборе generic plan возможны сюрпризы.

Индексы и autovacuum на партициях

Индекс, созданный на родительской таблице, автоматически создаётся на каждой партиции. Это удобно, но помните, что у каждой партиции свой физический индекс и своё состояние bloat. Для больших таблиц полезно настроить autovacuum агрессивнее на уровне партиции: параметры хранения не наследуются автоматически на уже созданные дочерние таблицы.

ALTER TABLE events_2026_01 SET (
    autovacuum_vacuum_scale_factor = 0.02,
    autovacuum_analyze_scale_factor = 0.01
);

CREATE INDEX events_source_idx ON events (source, created_at);

Индекс на родителе раскатывается на все партиции; на огромной схеме это долгая операция. Планируйте её на окно обслуживания или создавайте индекс с CONCURRENTLY на каждой партиции отдельно, а затем прикрепляйте к родителю через CREATE INDEX ON ONLY и ALTER INDEX ATTACH PARTITION.

Удаление и архивирование старых данных

Вместо DELETE с миллионами строк отключайте старую партицию. С PostgreSQL 14 это можно делать конкурентно, не блокируя запись в родительскую таблицу. Отсоединённая партиция становится обычной таблицей: её можно выгрузить, сжать или удалить.

ALTER TABLE events DETACH PARTITION events_2026_01 CONCURRENTLY;
pg_dump -Fc -t events_2026_01 appdb > events_2026_01.dump
DROP TABLE events_2026_01;

DETACH CONCURRENTLY нельзя выполнять внутри транзакции и не стоит совмещать с другими DDL в одном скрипте. Перед удалением убедитесь, что резервная копия действительно восстанавливается: проверка бэкапа важнее самого факта выгрузки. Для холодного архива подойдёт отдельная схема: партиция прикрепляется к таблице-архиву через ATTACH.

Типичные ошибки

  • Партиция по дате, а запросы фильтруют только по идентификатору пользователя: pruning не работает.
  • Отсутствие default-партиции и падение вставок на границе месяца; либо, наоборот, default-партиция, которая незаметно собирает мусор.
  • Уникальный индекс без ключа партиционирования и ошибка при создании схемы.
  • Слишком мелкие партиции: по дням за десять лет дадут тысячи файлов и медленное планирование.
  • Забытый autovacuum на партициях и рост bloat в индексах.

Default-партиция коварна тем, что при создании новой обычной партиции PostgreSQL должен проверить default на пересечение границ, а это полное сканирование. Если default нужен как страховка, держите его пустым и настройте алерт на появление строк.

Итог

Партиционирование PostgreSQL — это дисциплина эксплуатации, а не волшебная кнопка. Выберите естественный ключ, создавайте партиции заранее, проверяйте планы через EXPLAIN, настройте autovacuum и отключайте старые куски вместо массового DELETE. Тогда большая таблица остаётся управляемой, а удаление истории превращается в одну быструю операцию.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте