lenec ru

← все посты

PostgreSQL partitioning: секционирование таблиц для больших данных

12K

Когда таблица перерастает 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.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.