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

Партиционирование не ускоряет таблицу само по себе
В PostgreSQL 16 партиционирование помогает, когда запросы регулярно отбирают данные по ключу разделения, а старые части таблицы нужно отдельно обслуживать или удалять. Если выбрать неудобный ключ или нарезать таблицу на тысячи мелких секций, можно получить более долгие планы запросов, сложное обслуживание и неожиданные ошибки вставки.
Ниже — типичные сбои при работе с декларативным партиционированием PostgreSQL 16 и способы их предотвратить. Примеры используют события с датой и временем: для других данных принцип тот же, но ключ разделения выбирают под реальные запросы и обслуживание.
Ошибка 1. Выбрать ключ, по которому не фильтруют
Предположим, таблицу событий разделили по времени, а приложение чаще всего ищет события конкретного клиента за всё время. Такой запрос не ограничивает диапазон ключа разделения, поэтому PostgreSQL может обращаться ко многим секциям. Сам факт, что таблица разбита на части, не избавляет от чтения данных.
Перед выбором ключа соберите самые частые запросы и проверьте, какие условия в них стоят. Если типичный запрос выглядит как «клиент за период», разделение по времени может отсечь ненужные периоды. Если же приложение почти всегда выбирает клиента без периода, выигрыш будет не гарантирован: понадобятся подходящий индекс и проверка плана, а иногда партиционирование вообще не решает задачу.
Для временного ключа используйте границы диапазона без перекрытий. У секций RANGE нижняя граница включается, верхняя не включается. Это позволяет назначить записи ровно одной секции.
CREATE TABLE events (
event_id bigint GENERATED ALWAYS AS IDENTITY,
occurred_at timestamptz NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (occurred_at, event_id)
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2025_01
PARTITION OF events
FOR VALUES FROM ('2025-01-01 00:00:00+00')
TO ('2025-02-01 00:00:00+00');Это пример синтаксиса для PostgreSQL 16. Для проверки маршрутизации выполните запрос с датой внутри диапазона, затем проверьте соседние границы и дату, для которой секция ещё не создана.
Ошибка 2. Ожидать глобальную уникальность от каждой секции
PostgreSQL не может обеспечить уникальность ограничения по одному лишь event_id на всей секционированной таблице, если ключ разделения — occurred_at. Ограничение UNIQUE или PRIMARY KEY на родительской таблице должно включать все столбцы ключа разделения. Поэтому в примере первичный ключ составной: он включает и дату, и идентификатор.
Это меняет модель данных. Ссылка из другой таблицы только на event_id может оказаться недостаточной для ограничения внешнего ключа. Если по требованиям идентификатор должен быть уникальным глобально и его нужно независимо использовать в ссылках, продумайте отдельный механизм такой гарантии — например, отдельную таблицу реестра ключей — до миграции. Не полагайтесь только на то, что генератор идентификаторов обычно выдаёт разные значения: это не то же самое, что ограничение уникальности на всей таблице.
Ошибка 3. Создавать слишком много секций
Месячные секции удобны для многих временных данных, но не являются универсальным правилом. Если разрезать таблицу по минутам, секций станет очень много; если делать одну секцию на несколько лет, исчезнет удобство точечного удаления старых данных. У каждой секции свои метаданные и индексы, а планировщику нужно определить, какие из них участвуют в запросе. Большое число секций может увеличить накладные расходы планирования и усложнить обслуживание.
Выбирайте размер секции по частоте запросов, скорости поступления данных и политике хранения. Сравните планы на репрезентативном диапазоне данных, а не только на пустой тестовой таблице. Не назначайте универсальный предел по количеству секций: допустимое число зависит от схемы, запросов и ресурсов.
Ошибка 4. Не подготовить секцию для новых данных
Если подходящей секции нет, вставка строки в родительскую таблицу завершится ошибкой. Для потока событий это может означать остановку записи ровно в момент смены календарного периода. Заранее создайте будущие секции и автоматизируйте проверку их наличия. При этом проверьте, что задача создания успевает отработать до поступления первых строк нового диапазона.
Секция DEFAULT может принимать строки, которые не подошли ни одной обычной секции, но превращать её в бесконтрольный накопитель рискованно. Позже при добавлении секции PostgreSQL должен удостовериться, что её диапазон не пересекается с данными в DEFAULT. Если там накопились подходящие строки, добавление новой секции потребует сначала решить судьбу этих данных. Для критичного потока безопаснее сигнализировать о пропуске секции, чем молча прятать проблему в DEFAULT.
Ошибка 5. Считать индексы и планы такими же, как у обычной таблицы
Индекс, созданный на родительской секционированной таблице, задаёт индекс для секций; физически данные и индексы находятся на дочерних таблицах. При ручном создании или подключении секции проверьте, что нужные индексы есть и соответствуют ожиданиям. Иначе запросы к отдельной секции могут вести себя иначе, чем вы предполагали.
Для запросов по времени задавайте явный диапазон по ключу разделения. В PostgreSQL 16 планировщик умеет исключать ненужные секции на этапе планирования или выполнения — это называется отсечением секций. Но его наличие не отменяет проверки реального плана: выражения и приведения типов вокруг ключа, параметры запроса и форма условия могут повлиять на то, какие секции будут прочитаны.
EXPLAIN (ANALYZE, BUFFERS)
SELECT event_id, occurred_at
FROM events
WHERE occurred_at >= '2025-01-10 00:00:00+00'
AND occurred_at < '2025-01-11 00:00:00+00';Смотрите не только на общее время: проверьте план и фактическое число затронутых секций. ANALYZE выполняет запрос, поэтому на продуктивной системе учитывайте его нагрузку; сначала безопаснее проверить план на тестовых данных или выполнить запрос в подходящем окне.
Хорошее партиционирование начинается не с команды CREATE TABLE, а с ответа на два вопроса: по какому условию приложение читает данные и как команда будет удалять или обслуживать старые записи.
Когда выбирать RANGE, LIST или HASH
Тип разделения должен соответствовать фильтрам и операциям жизненного цикла данных. Сравнивайте варианты на своей схеме: ни один тип не гарантирует ускорение без подходящих запросов.
| Тип | Когда подходит | Цена и риск |
|---|---|---|
| RANGE | Данные имеют естественный порядок: время, числовые интервалы | Нужно заранее создавать диапазоны и не допускать пробелов |
| LIST | Небольшой набор устойчивых категорий, например регионов или типов | Новые значения требуют секции или заранее продуманной DEFAULT-секции |
| HASH | Нужно распределить строки по секциям по значению ключа | Плохо подходит для удаления данных по времени; изменение количества секций усложняет управление |
Что проверить перед запуском
- Сопоставьте ключ с запросами. Возьмите реальные запросы приложения и проверьте, ограничивают ли они ключ разделения. Результат — понятный набор основных сценариев чтения и обслуживания.
- Проверьте ограничения. Убедитесь, что нужные PRIMARY KEY и UNIQUE включают все столбцы ключа разделения. Результат — схема, которая обеспечивает именно те гарантии уникальности, на которые рассчитывает приложение.
- Смоделируйте границы и пропуски. Проверьте вставку на нижней и верхней границе секции, а также за пределами созданных диапазонов. Результат — строки попадают в ожидаемую секцию или ошибка возникает контролируемо.
- Проверьте план запроса. Выполните EXPLAIN на данных и запросах, похожих на рабочие. Результат — вы видите, какие секции и индексы действительно использует PostgreSQL.
- Отрепетируйте обслуживание. На тестовой схеме проверьте создание будущей секции и удаление или отсоединение старой. Результат — регламент не зависит от ручных действий в последний момент.
Термин: отсечение секций — исключение из плана тех секций, границы которых не подходят под условие запроса. Оно экономит чтение данных, но не заменяет индекс и не помогает, если запросу действительно нужны строки из многих секций.
Что делать дальше
Начните с одного самого важного запроса и одной операции хранения: например, выборки за период и удаления устаревших данных. Сравните план до и после на репрезентативном объёме, затем настройте мониторинг времени планирования, фактических секций в запросах, размера индексов и ошибок вставки из-за отсутствующей секции. Отдельная тема — миграция существующей большой таблицы; здесь разбирались проектирование и типовые ошибки уже секционированной таблицы.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: PostgreSQL 16: декларативное секционирование, PostgreSQL 16: ограничения целостности
Частые вопросы
Партиционирование автоматически ускорит SELECT?
Можно ли сделать PRIMARY KEY только по id?
Что произойдёт, если для новой даты нет секции?
Можно ли разделить таблицу, не меняя запросы приложения?
Подойдут ли эти примеры для PostgreSQL 15 или другой версии?
Что делать, если PostgreSQL всё равно читает много секций?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.