Партиционирование PostgreSQL: как спроектировать таблицу с нуля
Партиционирование в PostgreSQL помогает управлять большими таблицами, но само по себе не ускоряет запросы. Разбираем, когда выбрать RANGE, LIST или HASH, как задать ключ и создать месячные секции без ловушек с индексами и ограничениями.

В PostgreSQL 16 партиционирование не превращает медленный запрос в быстрый автоматически: выигрыш появляется, когда запросы отсекают ненужные секции, а обслуживание данных действительно упрощается. Ошибка в ключе может обернуться ограничениями на уникальность, пропущенными данными и десятками секций, которыми некому управлять. Ниже — практический вариант для таблицы событий и критерии, по которым выбрать другой способ.
Сначала проверьте, что партиционирование решает вашу задачу
Разбивать таблицу имеет смысл, если данные естественно делятся на части, а приложение часто работает с одной или несколькими из них. Например, сервис хранит события годами, но отчёты строит за конкретный месяц, а старые данные нужно регулярно удалять или архивировать.
Если запросы почти всегда читают всю таблицу, само разделение может не дать выигрыша. Оно добавляет операционную работу: нужно создавать секции заранее, следить за индексами и проверять, что планировщик исключает ненужные части. Перед изменением схемы посмотрите на реальные запросы и планы выполнения.
Хороший ключ партиционирования повторяет способ, которым приложение читает и обслуживает данные. Делить таблицу «на всякий случай» — значит добавлять сложность без гарантии ускорения.
Выберите стратегию по структуре данных
В PostgreSQL 16 есть три основных способа декларативного партиционирования. Выбирайте по смыслу ключа, а не по тому, какой вариант проще записать в DDL.
| Стратегия | Когда подходит | Цена и ограничения |
|---|---|---|
| RANGE | Даты, числовые интервалы, последовательные диапазоны значений | Нужно заранее создавать секции для новых диапазонов и продумывать границы. Часто подходит журналам событий и истории заказов. |
| LIST | Небольшой набор отдельных категорий: регион, тип данных, статус | Для нового значения нужна секция или заранее предусмотренная секция по умолчанию. Не подходит для поля с большим числом уникальных значений. |
| HASH | Равномерное распределение строк по фиксированному числу секций | Упрощает распределение, но не делит данные по времени или бизнес-смыслу. Удалить одним действием старый временной диапазон не получится. |
Для событий, которые выбирают и удаляют по дате, обычно рассматривают RANGE. LIST выбирают, когда сами категории важны для изоляции данных. HASH может распределить записи по частям, но его сложнее использовать для выборочного удаления и архивирования.
Пример: события с месячными секциями
Пример ниже рассчитан на PostgreSQL 16. В качестве ключа используется event_date, а диапазоны заданы так, чтобы нижняя граница входила в секцию, а верхняя — уже нет. У таблицы есть первичный ключ из даты события и идентификатора: в PostgreSQL уникальное ограничение или первичный ключ на партиционированной таблице должен включать все столбцы ключа партиционирования.
CREATE TABLE events (
event_date date NOT NULL,
id bigint NOT NULL,
account_id bigint NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (event_date, id)
) PARTITION BY RANGE (event_date);
CREATE TABLE events_2025_01
PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE events_2025_02
PARTITION OF events
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
CREATE INDEX events_account_date_idx
ON events (account_id, event_date);
Индекс создаётся на родительской таблице; PostgreSQL создаёт соответствующие индексы на её секциях. Когда вы добавите новую секцию, она также получит индекс, связанный с индексом родителя. Проверьте это после создания, особенно если используете собственные сценарии миграции или присоединяете уже существующую таблицу.
Пара ключевых ограничений здесь принципиальна. Во-первых, один и тот же id может встретиться в разных секциях: первичный ключ гарантирует уникальность пары (event_date, id), а не глобальную уникальность id. Во-вторых, секция должна быть создана до вставки данных для её диапазона. Декларативное партиционирование не создаёт будущие секции автоматически.
В рабочей системе запланируйте создание секций заранее — например, на горизонт, достаточный для вашего графика обслуживания. Конкретный запас выбирайте по частоте релизов и допустимому времени реакции на сбой. Если новых секций нет и подходящей секции по умолчанию тоже нет, вставка записи завершится ошибкой.
Что проверить до выкладки
- Запросы фильтруют по ключу. Для временной таблицы передавайте диапазон даты в условии запроса. Функция или преобразование над ключевым столбцом могут помешать исключению секций — проверьте план выполнения на реальном запросе.
- Уникальность соответствует схеме. Если приложению нужна глобальная уникальность
id, первичный ключ вместе с датой её не обеспечивает. Подберите другой способ идентификации или пересмотрите ключ партиционирования до переноса данных. - Для диапазонов нет пробелов и пересечений. Сверьте нижнюю и верхнюю границы соседних секций. Для ежедневной или месячной нарезки особенно легко ошибиться на границе периода.
- Число секций остаётся управляемым. Мелкая нарезка увеличивает объём метаданных и работу планировщика. Не выбирайте часовые секции только потому, что данные приходят часто: сначала проверьте размер таблицы, запросы и требования к удалению.
- Удаление старых данных безопасно. Удаление или отсоединение целой секции может быть удобнее построчного удаления, но это отдельная операция обслуживания. Проверьте её на копии схемы и убедитесь, что архивирование и внешние зависимости учтены.
Сноска. Отсечение секций, или partition pruning, — это исключение из плана запроса тех частей таблицы, которые не могут содержать нужные строки. Убедиться, что оно сработало, можно через EXPLAIN; для оценки фактического выполнения используют EXPLAIN ANALYZE на подходящей среде.
Если таблица уже существует
Обычную таблицу нельзя просто переключить на партиционирование одной настройкой: обычно создают новую родительскую таблицу с нужным ключом и секции, затем переносят данные и переключают приложение. Такой переход требует плана миграции, проверки ограничений и индексов, а также решения, как синхронизировать записи во время переноса.
Для большой таблицы можно подготовить отдельную таблицу с границами секции и присоединить её к родителю. Но PostgreSQL должен проверить, что все строки укладываются в заданный диапазон. Если таблица содержит неподходящие строки, операция не пройдёт; предварительное ограничение, подтверждающее границы, может помочь избежать повторной проверки всего содержимого. Детали зависят от схемы и версии, поэтому такой переход сначала отрабатывают на копии и сверяют с документацией PostgreSQL 16.
Если у родителя есть секция по умолчанию, присоединение новой секции тоже требует внимания: в секции по умолчанию не должно быть строк, попадающих в её диапазон. Иначе PostgreSQL должен проверить эти данные, а операция может потребовать длительной блокировки или завершиться ошибкой.
Когда выбирать каждый вариант
- Выбирайте RANGE, если типичный запрос ограничен диапазоном даты или числа и старые интервалы нужно обслуживать отдельно.
- Выбирайте LIST, если набор категорий невелик и каждая категория — самостоятельная единица обслуживания.
- Выбирайте HASH, если нужна раскладка строк по фиксированным группам, а операции с временными диапазонами не являются основной задачей.
Не начинайте с выбора числа секций. Сначала ответьте, по какому столбцу приложение фильтрует данные, какое ограничение уникальности ему нужно и как будут появляться и удаляться части таблицы. Если эти ответы не складываются в понятную схему, партиционирование пока не решает конкретную проблему.
Что делать дальше
Возьмите несколько самых частых запросов к таблице и проверьте их планы до изменения схемы. Затем на тестовой копии создайте родителя, две-три секции и индексы, загрузите репрезентативные данные и сравните планы через EXPLAIN ANALYZE. Перед запуском в продакшене отдельно проверьте создание следующей секции, вставку на границе периода, поведение уникальных ключей и процедуру удаления или архивации старых данных.
Карточки для маркетплейсов: Таблица товаров и папка фотографий на входе — комплект слайдов и описаний на выходе, по требованиям Wildberries и Ozon.
Спросить за 5 ₽Источники: PostgreSQL 16: декларативное партиционирование, PostgreSQL 16: CREATE TABLE
Частые вопросы
Партиционирование обязательно ускорит запросы?
Можно ли оставить первичный ключ только на id?
PostgreSQL сам создаст секцию на следующий месяц?
Что делать, если после перехода запрос всё ещё читает много секций?
Подойдёт ли этот пример для другой версии PostgreSQL?
Какие риски остаются в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.