PostgreSQL partitioning: секционирование таблиц для больших данных
Когда таблица перерастает 100 миллионов строк, привычные операции начинают деградировать. VACUUM работает часами, индексы раздуваются до десятков гигабайт, а удаление старых данных превращается в DELETE на миллионы строк с блокировками. Секционирование (partitioning) решает эти проблемы, разбивая одну логическую таблицу на физически отдельные части.
Зачем partitioning: проблема больших таблиц
На таблице в 500M строк вы столкнётесь с:
- Медленный VACUUM — обходит всю таблицу, даже если изменилось 0.1% строк.
- Раздутые индексы — B-tree на 500M строк занимает 10-30 ГБ, обновление тормозит INSERT.
- Удаление старых данных —
DELETE WHERE created_at < '2024-01-01'генерирует WAL на десятки ГБ и блокирует таблицу. - Бэкапы — pg_dump одной огромной таблицы занимает часы.
С партициями: VACUUM работает только с активными партициями, удаление старых данных — это мгновенный DROP TABLE, индексы маленькие и быстрые.
Типы: RANGE, LIST, HASH
RANGE — самый популярный. Разбивает по диапазону значений. Идеален для временных рядов:
CREATE TABLE events (
id bigserial,
created_at timestamptz NOT NULL,
event_type text,
payload jsonb
) PARTITION BY RANGE (created_at);
LIST — разбивает по конкретным значениям. Подходит для мультитенантных систем или статусов:
CREATE TABLE orders (
id bigserial,
region text NOT NULL,
total numeric
) PARTITION BY LIST (region);
HASH — равномерно распределяет строки по N партициям. Используется когда нет естественного ключа для RANGE/LIST, но нужно разбить таблицу для параллелизма:
CREATE TABLE sessions (
id uuid NOT NULL,
user_id bigint,
data jsonb
) PARTITION BY HASH (id);
Создание партиций
После объявления родительской таблицы создаём дочерние:
-- RANGE: помесячные партиции
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');
-- LIST: по регионам
CREATE TABLE orders_eu PARTITION OF orders
FOR VALUES IN ('eu-west', 'eu-east', 'eu-central');
CREATE TABLE orders_us PARTITION OF orders
FOR VALUES IN ('us-east', 'us-west');
-- HASH: 4 партиции
CREATE TABLE sessions_0 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
Если строка не попадает ни в одну партицию — INSERT упадёт с ошибкой. Создавайте партиции заранее или используйте DEFAULT:
CREATE TABLE events_default PARTITION OF events DEFAULT;
Автоматизация: pg_partman
Создавать партиции вручную каждый месяц — рутина. pg_partman автоматизирует это:
CREATE EXTENSION pg_partman;
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'created_at',
p_type := 'native',
p_interval := 'monthly',
p_premake := 3 -- создать 3 будущих партиции заранее
);
-- Вызывать по cron (pg_cron или системный):
SELECT partman.run_maintenance();
-- Автоудаление старых партиций:
UPDATE partman.part_config
SET retention = '12 months', retention_keep_table = false
WHERE parent_table = 'public.events';
pg_partman сам создаёт новые партиции и удаляет старые по retention.
Индексы на партициях
В PostgreSQL индексы на партиционированных таблицах — локальные. Каждая партиция имеет свой индекс:
-- Индекс на родительской таблице автоматически создаётся на всех партициях
CREATE INDEX idx_events_type ON events (event_type);
-- Это создаст:
-- idx_events_type_events_2026_01
-- idx_events_type_events_2026_02
-- ... и на каждой будущей партиции
Глобальных индексов (spanning all partitions) в PostgreSQL нет. Уникальность можно гарантировать только если ключ партиционирования входит в уникальный индекс:
-- Работает: partition key (created_at) в составе UNIQUE
CREATE UNIQUE INDEX ON events (id, created_at);
-- НЕ работает: уникальность без partition key
-- CREATE UNIQUE INDEX ON events (id); -- ERROR
Подводные камни
- Constraint exclusion. Планировщик отсекает ненужные партиции только если WHERE содержит partition key. Запрос без фильтра по
created_atпросканирует ВСЕ партиции. - UPDATE с переносом строки. Если UPDATE меняет значение partition key так, что строка должна переехать в другую партицию — PostgreSQL сделает DELETE + INSERT. Это работает с PG 11+, но медленнее обычного UPDATE.
- Foreign keys. FK на партиционированную таблицу поддерживается с PG 12+. FK от партиционированной таблицы к обычной — работает всегда.
- Слишком много партиций. Тысячи партиций замедляют планирование. Оптимально — десятки-сотни.
- pg_dump. Для больших баз используйте
pg_dump -j Nдля параллелизма.
Вывод
Секционирование — не серебряная пуля, а инструмент для конкретных проблем: быстрое удаление старых данных, ускорение VACUUM, уменьшение индексов. Начинайте с RANGE по времени — это покрывает 80% случаев. Используйте pg_partman для автоматизации и всегда проверяйте, что ваши запросы содержат фильтр по partition key.