Партиционирование в PostgreSQL

Предпосылки: партиционирование (концепция, выбор ключа), таблицы и типы (CREATE TABLE).

EXCLUSION | Материализованные представления

В партиционировании рассмотрена идея: логическая таблица из физических частей, DROP вместо DELETE. PostgreSQL реализует её через декларативный DDL — PARTITION BY.

Декларативное партиционирование

CREATE TABLE events (
    id         BIGSERIAL,
    created_at TIMESTAMP NOT NULL,
    user_id    BIGINT NOT NULL,
    payload    JSONB NOT NULL
) PARTITION BY RANGE (created_at);
 
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');

Для приложения таблица остаётся events: INSERT и SELECT пишутся как обычно. PostgreSQL сам выбирает нужную партицию при вставке и отфильтровывает лишние при чтении.

Типы партиционирования

RANGE — по диапазону значений. Подходит для дат и последовательных данных. Самый частый тип.

LIST — по конкретным значениям:

CREATE TABLE orders (
    id     BIGINT,
    region TEXT NOT NULL,
    total  INTEGER
) PARTITION BY LIST (region);
 
CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('US');
CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN ('EU', 'UK');

HASH — по хешу значения. Равномерное распределение строк по партициям:

PARTITION BY HASH (user_id);
-- MODULUS (англ. «модуль») и REMAINDER (англ. «остаток») задают хеш-распределение
CREATE TABLE events_p0 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE events_p1 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 1);

Partition pruning — отсечение ненужных партиций

PostgreSQL автоматически пропускает партиции, не содержащие нужных строк:

SELECT *
FROM events
WHERE created_at >= '2026-02-10'
  AND created_at <  '2026-02-11';

PostgreSQL читает только events_2026_02. При 12 месячных партициях запрос по одному месяцу читает 1/12 данных.

Ограничения уникальности

PostgreSQL требует, чтобы уникальные ограничения на партиционированной таблице включали ключ партиционирования. UNIQUE(id) без created_at невозможен — уникальность проверяется только внутри одной партиции:

-- Ошибка: UNIQUE(id) невозможен
-- Допустимо:
ALTER TABLE events ADD PRIMARY KEY (id, created_at);

Retention — мгновенное удаление

DROP TABLE events_2025_11 вместо DELETE FROM events WHERE created_at < '2025-12-01'. DROP — мгновенная DDL-операция: нет сканирования строк, нет нагрузки на фоновые процессы очистки.

Sub-partitioning

Таблица events партиционирована по created_at (помесячно). Появляется новое требование: «все события customer_id = 42 за год». Запрос WHERE customer_id = 42 AND created_at >= '2025-01-01' ограничивает pruning до 12 партиций, но внутри каждой — полный скан по customer_id.

RANGE(created_at) на первом уровне, HASH(customer_id) на втором. 12 месяцев × 8 хеш-партиций = 96 партиций. Retention работает (DROP целого месяца), запросы по customer_id сужаются до одной хеш-партиции внутри каждого месяца.

Цена: больше партиций — больше файлов, дольше планирование. При сотнях и тысячах партиций overhead планировщика становится ощутимым.

ATTACH и DETACH PARTITION

ATTACH PARTITION — присоединение

ALTER TABLE events ATTACH PARTITION events_2026_03
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');

ATTACH PARTITION берёт SHARE UPDATE EXCLUSIVE на родительскую таблицу и ACCESS EXCLUSIVE на дочернюю. Если в дочерней таблице данные не проверены, PostgreSQL сканирует её, чтобы убедиться, что все строки удовлетворяют constraint партиции — на большой таблице это долгая блокировка.

Решение: сначала добавить CHECK constraint на дочернюю таблицу, потом ATTACH. PostgreSQL пропускает проверку, если CHECK уже гарантирует соответствие:

ALTER TABLE events_2026_03
ADD CONSTRAINT check_range CHECK (
    created_at >= '2026-03-01' AND created_at < '2026-04-01'
);
 
-- Теперь ATTACH мгновенный -- проверка пропущена
ALTER TABLE events ATTACH PARTITION events_2026_03
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');

DETACH PARTITION — отсоединение

ALTER TABLE events DETACH PARTITION events_2025_11;
DROP TABLE events_2025_11;

DETACH PARTITION CONCURRENTLY (PostgreSQL 14+) — неблокирующий detach. Не берёт ACCESS EXCLUSIVE на родительскую таблицу, но нельзя использовать внутри транзакции.

Sources


EXCLUSION | Материализованные представления