Представьте таблицу событий, которая накапливает по несколько миллионов строк в день. Через год в ней миллиарды записей. Запросы замедляются, автоматическая очистка не успевает, а удаление старых данных через DELETE превращается в часовую операцию с огромной нагрузкой на диск. Это именно та ситуация, для которой создано партиционирование.
Что такое партиционирование
Партиционирование — это разделение одной большой таблицы на несколько физических частей (партиций) по заданному правилу. Снаружи вы продолжаете работать с одной таблицей: делаете INSERT, SELECT, UPDATE как обычно. PostgreSQL сам решает, в какую партицию записать строку и какие партиции просматривать при запросе.
Главное — понять разницу между «одна логическая таблица» и «несколько физических файлов на диске». Когда вы пишете:
SELECT * FROM event_log WHERE occurred_at >= '2026-05-01' AND occurred_at < '2026-06-01';
PostgreSQL видит, что запрос касается только мая, и читает только одну партицию event_log_2026_05. Остальные месяцы физически не трогаются. Это называется partition pruning (отсечение партиций).
Когда партиционирование оправдано
Партиционирование решает конкретные проблемы. Без проблемы — инструмент не нужен.
Стоит рассмотреть, если:
- Таблица больше 50 GB (или 100 миллионов строк) и продолжает расти.
- Это данные типа time-series: события, метрики, логи — где у каждой строки есть временная метка.
- Старые данные нужно регулярно удалять: за прошлый месяц, за прошлый год.
- Автоочистка (autovacuum) не справляется с таблицей — постоянно отстаёт.
Лишнее, если:
- Таблица меньше 10 GB. Обычные индексы справятся лучше и без сложности.
- Запросы редко используют предполагаемый ключ партиционирования в
WHERE. Тогда отсечения партиций не будет — только накладные расходы. - Данные распределены абсолютно равномерно и запросы читают всё подряд — это не партиционирование, это скорее шардирование.
Декларативное партиционирование
С PostgreSQL 10 появился удобный синтаксис. Сначала создаёте «родительскую» таблицу с указанием типа партиционирования, затем добавляете партиции:
CREATE TABLE event_log (
id bigint GENERATED ALWAYS AS IDENTITY,
occurred_at timestamptz NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (id, occurred_at)
) PARTITION BY RANGE (occurred_at);
CREATE TABLE event_log_2026_05 PARTITION OF event_log
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE event_log_2026_06 PARTITION OF event_log
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
Обратите внимание: первичный ключ включает occurred_at — поле партиционирования. Это обязательное требование PostgreSQL. Без этого создать PK не получится.
После этого всё работает прозрачно:
-- INSERT идёт в родительскую таблицу, PostgreSQL сам кладёт в нужную партицию
INSERT INTO event_log (occurred_at, payload) VALUES (now(), '{"type":"click"}');
-- SELECT с отсечением партиций — читается только event_log_2026_05
SELECT * FROM event_log
WHERE occurred_at >= '2026-05-15' AND occurred_at < '2026-05-20';
Три типа партиционирования
RANGE — для временных данных
Самый распространённый тип. Каждая партиция отвечает за диапазон значений — например, один месяц или один год.
PARTITION BY RANGE (occurred_at);
Подходит для time-series, архивов по периодам, числовых шкал.
LIST — для категорий
Когда данные делятся по фиксированному набору значений: регион, тип документа, идентификатор клиентской группы.
CREATE TABLE order_doc (...) PARTITION BY LIST (region);
CREATE TABLE order_doc_eu PARTITION OF order_doc FOR VALUES IN ('EU', 'UK');
CREATE TABLE order_doc_usa PARTITION OF order_doc FOR VALUES IN ('US', 'CA');
CREATE TABLE order_doc_other PARTITION OF order_doc DEFAULT;
Секция DEFAULT принимает все строки, которые не попали ни в одну из именованных партиций. Без неё INSERT с неизвестным значением упадёт с ошибкой.
HASH — равномерное распределение
Партиции определяются по остатку от деления хэша поля на число партиций.
PARTITION BY HASH (user_id);
CREATE TABLE user_event_p0 PARTITION OF user_event FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_event_p1 PARTITION OF user_event FOR VALUES WITH (MODULUS 4, REMAINDER 1);
-- и т.д.
Используется редко. HASH не даёт отсечения партиций при диапазонных запросах — только при точном совпадении (WHERE user_id = ?). Помогает равномерно распределить нагрузку на запись, но почти никогда не применяется на практике для запросов с фильтрацией по диапазону.
Как выбрать ключ партиционирования
Выбор ключа — самое важное решение при партиционировании. Неправильный ключ сведёт пользу к нулю.
Правило одно: ключ должен быть в WHERE почти всех запросов к этой таблице.
Хорошие примеры:
- Таблица событий с
WHERE occurred_at > ?→ ключoccurred_at. - Многопользовательская система, где каждый запрос идёт в контексте одного клиента → ключ
tenant_id. - Архив заказов с запросами за период → ключ
created_at.
Плохие примеры:
- Партиционировать
ordersпоstatus, если большинство запросов фильтрует поcustomer_id— отсечения не будет, PostgreSQL будет читать все партиции. - Партиционировать
customersпоcountry, если 90% записей в одной стране — одна партиция будет огромной, остальные почти пустыми.
Перед созданием партиций полезно проверить распределение данных:
SELECT date_trunc('month', occurred_at) AS m, count(*)
FROM event_log GROUP BY m ORDER BY m;
Если данные распределены неравномерно — переосмыслите ключ.
Размер партиций
Ориентируйтесь на 1–50 GB на партицию. Для time-series это означает:
- Помесячные партиции: при нескольких миллионах событий в месяц.
- Понедельные: при десятках миллионов событий в неделю.
- Подневные: при сотнях миллионов событий в день.
Важный момент по количеству партиций: при более 1000 партиций планировщик начинает заметно тормозить. При более 10 000 — кластер работает очень медленно. Если нужны подневные партиции за несколько лет, заранее думайте об архивировании.
Управление партициями
Создавайте партиции заранее
Если строка попадает в таблицу, а нужной партиции ещё нет — INSERT упадёт с ошибкой (если нет DEFAULT партиции). Заводите партиции на 1–2 периода вперёд.
CREATE TABLE event_log_2026_07 PARTITION OF event_log
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
Автоматизировать это можно через расширение pg_partman:
CREATE EXTENSION pg_partman;
SELECT partman.create_parent(
p_parent_table => 'public.event_log',
p_control => 'occurred_at',
p_type => 'native',
p_interval => '1 month',
p_premake => 4 -- создать 4 партиции наперёд
);
-- вызывается по расписанию (cron или pg_cron)
SELECT partman.run_maintenance('public.event_log');
pg_partman создаёт новые партиции автоматически и может удалять устаревшие по настраиваемым правилам хранения.
Мгновенное удаление старых данных — главный плюс
Вот ради чего обычно и партиционируют time-series данные:
DROP TABLE event_log_2025_05; -- мгновенно, без нагрузки
Сравните с обычным удалением:
DELETE FROM event_log WHERE occurred_at < '2026-01-01';
При миллионах строк этот DELETE:
- Делает полное сканирование для поиска строк.
- Создаёт огромное количество MVCC-маркеров (PostgreSQL не удаляет строки физически сразу).
- Требует запуска автоочистки для освобождения места на диске.
- Может занять десятки минут с высокой нагрузкой на диск и WAL.
DROP TABLE партиции удаляет файл с диска мгновенно — без сканирования, без MVCC, без WAL.
Отделение партиции без удаления
Иногда нужно не удалить старые данные, а переместить их — например, в архивную таблицу или другое хранилище:
ALTER TABLE event_log DETACH PARTITION event_log_2025_05;
-- теперь event_log_2025_05 — обычная самостоятельная таблица
В PostgreSQL 14+ появился вариант без блокировки родительской таблицы:
ALTER TABLE event_log DETACH PARTITION event_log_2025_05 CONCURRENTLY;
Индексы на партиционированных таблицах
Индекс, созданный на родительской таблице, автоматически создаётся на каждой партиции:
CREATE INDEX ON event_log (payload->>'event_type');
-- PostgreSQL создаёт индекс на event_log_2026_05, event_log_2026_06 и т.д.
Для уникальных индексов есть ограничение: они должны включать ключ партиционирования. Уникальность гарантируется только в пределах одной партиции. Если нужна глобальная уникальность (например, по UUID), заведите отдельную справочную таблицу.
Частые ошибки
Партиционировать маленькую таблицу. Если данных меньше 10 GB, партиционирование добавляет сложность без ощутимой пользы. Хорошие индексы и настроенный autovacuum справятся лучше.
Ключ не используется в запросах. Если запросы не фильтруют по ключу партиционирования, PostgreSQL читает все партиции подряд — это медленнее, чем обычная таблица с индексом.
Слишком много партиций. Подневное партиционирование за несколько лет легко даст тысячи партиций. Помните про ограничение ~1000 партиций для нормальной работы планировщика.
Не создать партицию заранее. INSERT без нужной партиции упадёт. Используйте pg_partman или скрипт в задании планировщика.
Менять значение поля партиционирования через UPDATE. PostgreSQL фактически удаляет строку из одной партиции и вставляет в другую — это неэффективно и может вызвать неожиданные блокировки. Ключ партиционирования должен быть неизменяемым.
Миграция существующей таблицы в партиционированную
Если таблица уже существует и разрослась, её можно переместить в партиционированную структуру:
-- 1. Создать новую партиционированную таблицу
CREATE TABLE event_log_new (LIKE event_log INCLUDING ALL)
PARTITION BY RANGE (occurred_at);
-- Создать партиции...
-- 2. Скопировать данные
INSERT INTO event_log_new SELECT * FROM event_log;
-- 3. Переименовать атомарно
BEGIN;
ALTER TABLE event_log RENAME TO event_log_old;
ALTER TABLE event_log_new RENAME TO event_log;
COMMIT;
DROP TABLE event_log_old;
Для таблицы в 500 GB копирование займёт несколько часов, и потребуется место под обе версии одновременно. Для продакшн-окружений без даунтайма схема сложнее.
Коротко
- Партиционирование — это одна логическая таблица, разбитая на несколько физических по заданному правилу.
- Главный плюс для time-series:
DROP TABLEстарой партиции мгновенен, тогда какDELETEмиллионов строк занимает десятки минут. - Ориентиры для применения: таблица больше 50 GB, time-series, регулярное удаление старых данных, autovacuum не справляется.
- Три типа: RANGE (временные данные, самый частый), LIST (категории), HASH (редко, только для равномерной записи).
- Ключ партиционирования должен быть в
WHEREбольшинства запросов — иначе partition pruning не работает. - Первичный ключ обязан включать поле партиционирования.
- Целевой размер партиции: 1–50 GB; более 1000 партиций тормозит планировщик.
pg_partmanавтоматизирует создание новых партиций и удаление устаревших.- Не партиционируйте таблицы меньше 10 GB — обычные индексы справятся лучше.
Что почитать дальше
- Автовакуум в PostgreSQL — как autovacuum работает с партиционированными таблицами.
- Materialized views — альтернатива для агрегатов и отчётов.
- Multi-tenancy паттерны — когда партиции по
tenant_idпротив row-per-tenant с RLS. - Миграции без даунтайма — как безопасно переехать на партиционированную таблицу в продакшне.