Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Секционирование таблиц в PostgreSQL: когда и как

Партиционирование в PostgreSQL: declarative-партиции по диапазону и списку

Когда таблице нужны партиции: declarative partitioning по RANGE/LIST/HASH, создание и обслуживание, partition pruning в планах и типичные ошибки разработчика.

Прямой ответ

Секционирование (declarative partitioning) разбивает одну логическую таблицу на физические куски-партиции; разработчику оно даёт три вещи: отсечение в планах (partition pruning — запрос с WHERE created_at >= '2026-10-01' сканирует только партиции октября — чтение планов: «Subplans Removed: N» — доказательство отсечения), дёшевое удаление старого (DROP TABLE events_2025_09 за миллисекунды против часов DELETE + вакуумной уборки — bloat для append-heavy данных устраняется целиком), обслуживание по кускам (VACUUM/REINDEX/ANALYZE отдельной партиции — короткие операции вместо бесконечных на монолите). Порог входа: партиции оправданы от ~десятков-сотен миллионов строк или когда жизненный цикл данных «устаревает по времени» (логи/события/аудит — классика RANGE по created_at); на миллионах строк обычная таблица с индексами быстрее и проще (overhead планирования по партициям — реален: сотни-тысячи партиций замедляют планирование запросов — «партицировать всё заранее» — антипаттерн). Механика: CREATE TABLE events (...) PARTITION BY RANGE (created_at) + партиции PARTITION OF events FOR VALUES FROM ('2026-10-01') TO ('2026-11-01'); ключ партиционирования входит в PK/UNIQUE (ограничение Postgres — уникальность только внутри ключа+партиций); новые партиции создаются заранее (cron/pg_partman — вставка в несуществующий диапазон — ошибка без DEFAULT-партиции, а DEFAULT-партиция с данными тормозит pruning — держать пустой или не иметь вовсе); индексы — на родительской таблице (наследуются партициями автоматически), запросы — всегда с фильтром по ключу партиционирования (иначе — скан всех партиций — хуже монолита).

Когда партиционировать: признаки и границы

Признаки «пора»: таблица >100M строк и растёт; регулярное удаление старых данных (DELETE по времени — долгий, блокирующий, раздувающий — замена на DROP партиции — главный практический выигрыш); запросы почти всегда фильтруют по одному измерению (время/tenant/регион — ключ партиционирования лежит на поверхности); обслуживание (VACUUM/REINDEX) таблицы занимает часы. Не партиционировать, когда: данных <10–50M строк (индексы решают — партиции добавят планировочный overhead без выигрыша); запросы не фильтруют по ключу (кросс-партиционные сканы/JOIN без отсечения — медленнее монолита); нужна глобальная уникальность не по ключу партиционирования (PK обязан включать ключ — email уникальным по всей таблице при партициях по времени не сделать — знать заранее); «для ускорения всего» — партиции ускоряют целевые паттерны, не всё подряд. Выбор стратегии: RANGE по времени (90% случаев — логи/события/заказы-архив; шаг партиции — месяц/неделя по объёму: цель — партиция на 1–10 ГБ), LIST по дискретному ключу (tenant_id/страна — «партиция на клиента» в multi-tenant — многоарендность — при умеренном числе значений), HASH (равномерное дробление без естественного ключа — удаление старого не даёт — ниша); DEFAULT-партиция — страховка от «вставка вне диапазонов», но с оговоркой (pruning проверяет DEFAULT — держать пустой; лучше — генерация партиций с запасом + алерт).

Механика: создание, индексы, ограничения

DDL: родитель PARTITION BY RANGE (created_at) — сам данных не хранит; партиции — CREATE TABLE events_2026_10 PARTITION OF events FOR VALUES FROM ('2026-10-01') TO ('2026-11-01') (границы полуинтервал — нижняя включительно); PK/UNIQUE: PRIMARY KEY (id, created_at) — ключ партиционирования обязан входить (иначе ошибка — уникальность «сквозь партиции» Postgres не обеспечивает; бизнес-уникальность «один заказ на email» — приложением/отдельной таблицей — ограничение осознать до миграции); индексы — создавать на родителе (CREATE INDEX ON events (user_id, created_at)) — партиционные индексы создадутся/поддержатся автоматически (и REINDEX родителя — по партициям последовательно); FK на партицированную таблицу — поддерживаются (с 12+), но FK с партицированной таблицы на обычные — тоже ок; attach существующей таблицы — ATTACH PARTITION ... FOR VALUES ... (миграция монолита кусками: новые данные в партиции, старое переносится батчами и аттачится — с CONSTRAINT-предпроверкой чтобы не держать долгую блокировку — сценарий ниже); детач — DETACH PARTITION ... CONCURRENTLY (без блокировки родительских запросов — подготовка к DROP/архиву); партиции — полноценные таблицы (свои статистики/autovacuum-настройки/tablespace — горячие в быстром tablespace, архив в дешёвом). Планирование: pruning работает по константам и параметризованным условиям (prepared-параметры — runtime pruning — с 11); функции над ключом (WHERE date_trunc('month', created_at) = ...) — pruning ломают (фильтр по «сырой» колонке — правило для всей команды — индексная дисциплина та же).

Эксплуатация: генерация партиций, удаление, мониторинг

Создание будущих партиций — автоматизировать с первого дня: pg_partman (расширение — генерация/ротация по политикам — стандарт де-факто) или свой cron-скрипт (CREATE TABLE IF NOT EXISTS ... PARTITION OF ... на 3 месяца вперёд — идемпотентно); пропуск — вставки падают (или валятся в DEFAULT и убивают pruning — обе беды заметны сразу — алерт «партиция на следующий месяц отсутствует» — простейший мониторинг). Удаление старого: DETACH CONCURRENTLY → (опционально) перенос в архивную БД/хранилище (экспорт/S3) → DROP — секунды вместо часов DELETE; политика хранения становится DDL-расписанием (retention как код — IaC-мышление). Мониторинг: размеры партиций (перекос — признак плохого ключа), «самая старая партиция» (соответствие retention), количество партиций (тысячи — планирование деградирует — укрупнять шаг или архивировать активнее), наличие DEFAULT-данных, доля запросов без ключа партиционирования (медленный лог/pg_stat_statements — запросы, сканирующие все партиции — кандидаты на переписывание). Статистики — ANALYZE на свежих партициях (свежая партиция с нулевой статистикой — планы наугад — autovacuum-analyze догонит, но на горячих — вручную после массовой загрузки). Миграция существующей большой таблицы — отдельный проект: партицированная таблица создаётся рядом, новые записи — в обе (триггер/dual write) или переключением, история переносится батчами и ATTACH-ится — с проверкой контрольных сумм; «переделать таблицу на партиции за вечер» — на сотнях миллионов строк так не бывает — планировать окно/инструменты (pg_partman-миграции — батчами).

Практический сценарий: таблица событий на 900M строк

Исходник: events — 900M строк, 420 ГБ с индексами; еженедельная чистка «данные старше 180 дней» — DELETE по 40M строк — 3 часа, реплика отстаёт, vacuum после — ещё часы; аналитические запросы «за последнюю неделю» — 20+ с. Решение: RANGE по created_at, шаг — месяц (партиция ~35 ГБ), PK (id, created_at) — приложению id достаточно, уникальность id глобальна по факту (sequence). Миграция (окно 2 недели, без простоя): новая events_p партицированная; dual-write триггером (новые события в обе — двойная запись-подобная осторожность: ошибки триггера — в лог, не в отказ записи); история — батчами по 5M строк в партиции по месяцам (INSERT INTO ... SELECT + ATTACH предзаполненных — с constraint-проверкой до attach, чтобы блокировка была мгновенной); переключение — переименование в транзакции (events → events_old, events_p → events — секунды); откат готов (events_old жива неделю). Итоги: чистка retention — DETACH CONCURRENTLY + DROP — 200 мс вместо 3 часов (реплика счастлива — репликация); «за последнюю неделю» — 1.8 с (pruning: 1–2 партиции — видно в EXPLAIN «Subplans Removed: 35»); VACUUM/ANALYZE — по партициям, autovacuum per-table на свежих агрессивнее; генерация партиций — pg_partman на +3 месяца, алерт на отсутствие будущей. Ошибка, которую поймали на стейдже: аналитический запрос фильтровал date_trunc('month', created_at) — сканировал все партиции — переписан на диапазон — pruning заработал (правило «фильтр по сырой колонке» — в ревью-чеклист — чек-лист).

FAQ

Партиционирование против табличного пространства и архивных таблиц вручную? Партиции — прозрачны для запросов (одна логическая таблица, pruning автоматом) — вручную разнесённые таблицы-архивы требуют UNION VIEW и дисциплины (или приложения) — партиции это встроенный механизм того же; tablespace — про расположение файлов, не про структуру — дополняют, не заменяют; «своя схема шардинга по таблицам events_2026_10» — самописные партиции без pruning/PK-наследования — делать встроенными.

Сколько партиций — слишком много? Практический ориентир: сотни партиций — комфортно; тысячи — планирование запросов заметно тяжелеет (каждый запрос оценивает партиции — даже с pruning — метаданные и append-ноды); лечение — укрупнение шага (месяц → квартал для холодных), архивация/DETACH старых, и partition-wise JOIN/агрегации не спасут от мета-объёма; «партиция на каждый день на 10 лет вперёд» — 3650 партиций — ошибка проектирования, считать от объёма.

Ускоряет ли партиционирование точечный SELECT по id? Нет, а может замедлить: без ключа партиционирования в запросе — индексный поиск идёт по каждой партиции (N индексных спусков вместо одного); точечные запросы по id — либо включать created_at в запрос (pruning → одна партиция), либо держать отдельный глобальный справочник id→партиция, либо не партиционировать таблицы, живущие точечными выборками; партиции — про диапазоны/массовые операции/жизненный цикл.

Можно ли партицировать существующую таблицу «на месте»? Напрямую — нет (ALTER TABLE ... PARTITION BY не существует): пути — новая партицированная + перенос (сценарий выше — dual write или окно), pg_partman-процедуры «partition_data_proc» для батч-миграции, логическая репликация в новую структуру (публикация → подписка → свитч — без остановки записи — на самых больших объёмах); выбор по размеру и допустимому окну — на десятках миллионов строк окно в час реально, на миллиардах — только онлайн-схемы.

Источники: документация PostgreSQL (DDL partitioning, maintenance, query config) — ссылки в шапке.