Когда таблица вырастает до десятков гигабайт, обычные индексы перестают спасать. Секционирование разбивает большую таблицу на физически отдельные части — секции — сохраняя единый интерфейс для запросов.
Когда нужно секционирование
- Таблица занимает больше 50–100 ГБ и растёт
- Большинство запросов фильтруют по одной колонке (дата, регион, статус)
- Нужно быстро удалять старые данные (например, логи старше года)
- Загрузка данных идёт батчами по временным диапазонам
Не стоит секционировать маленькие таблицы — накладные расходы не окупятся.
PARTITION BY RANGE — по диапазону
Самый частый случай: секционирование по дате. Каждая секция хранит данные за один период.
-- Создаём секционированную таблицу
CREATE TABLE orders (
id BIGSERIAL,
user_id INT,
total NUMERIC,
status TEXT,
created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);
-- Создаём секции по месяцам
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_2026_03 PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- DEFAULT-секция для строк не попавших ни в одну секцию
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
Граница TO не включается — строка с created_at = '2026-02-01' попадёт в orders_2026_02, а не в orders_2026_01.
-- INSERT автоматически попадает в нужную секцию
INSERT INTO orders (user_id, total, status, created_at)
VALUES (1, 5000, 'paid', '2026-02-15');
-- → пошёл в orders_2026_02
-- SELECT автоматически читает только нужные секции (partition pruning)
SELECT * FROM orders WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';
-- → читает только orders_2026_02
Partition Pruning — главное преимущество
PostgreSQL анализирует условия WHERE и пропускает секции, в которых заведомо нет нужных данных. Это называется partition pruning:
EXPLAIN SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01';
Append
-> Seq Scan on orders_2026_01
Filter: (created_at >= '2026-01-01' AND created_at < '2026-02-01')
-- orders_2026_02, orders_2026_03 не читаются вообще!
Без секционирования пришлось бы сканировать всю таблицу. С секционированием — только нужный месяц.
PARTITION BY LIST — по списку значений
Подходит для дискретных значений: регион, статус, категория.
CREATE TABLE events (
id BIGSERIAL,
region TEXT NOT NULL,
event_type TEXT,
data JSONB,
created_at TIMESTAMPTZ
) PARTITION BY LIST (region);
CREATE TABLE events_russia PARTITION OF events
FOR VALUES IN ('russia', 'RU');
CREATE TABLE events_europe PARTITION OF events
FOR VALUES IN ('germany', 'france', 'spain', 'DE', 'FR', 'ES');
CREATE TABLE events_other PARTITION OF events DEFAULT;
-- Запрос читает только европейские секции
SELECT COUNT(*) FROM events WHERE region IN ('germany', 'france');
-- → Читает только events_europe
PARTITION BY HASH — равномерное распределение
Когда нет естественного ключа для разбивки, HASH распределяет строки равномерно по фиксированному числу секций:
CREATE TABLE user_activity (
id BIGSERIAL,
user_id INT NOT NULL,
action TEXT,
created_at TIMESTAMPTZ
) PARTITION BY HASH (user_id);
-- 4 секции: MODULUS = количество секций, REMAINDER = номер секции (0..N-1)
CREATE TABLE user_activity_0 PARTITION OF user_activity
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_activity_1 PARTITION OF user_activity
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE user_activity_2 PARTITION OF user_activity
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE user_activity_3 PARTITION OF user_activity
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
Hash-партиционирование не даёт pruning по диапазонам, но позволяет распараллелить нагрузку по нескольким физическим хранилищам.
Индексы на секционированных таблицах
-- Индекс на родительской таблице создаётся на всех секциях автоматически
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- → Создаст idx на orders_2026_01, orders_2026_02, orders_2026_03...
-- Индекс только на конкретной секции
CREATE INDEX ON orders_2026_01 (user_id);
-- PRIMARY KEY и UNIQUE должны включать ключ партиционирования
-- Иначе PostgreSQL не сможет гарантировать уникальность
CREATE UNIQUE INDEX ON orders (id, created_at);
Управление секциями: DETACH и ATTACH
Самая полезная операция секционирования — мгновенное удаление старых данных:
-- Отсоединить секцию (данные остаются в таблице, просто отвязываются)
ALTER TABLE orders DETACH PARTITION orders_2024_01;
-- Теперь orders_2024_01 — обычная таблица, можно делать что угодно
DROP TABLE orders_2024_01; -- мгновенно, не DELETE по строкам!
-- Или заархивировать:
-- COPY orders_2024_01 TO '/archive/orders_2024_01.csv'
-- DROP TABLE orders_2024_01;
-- Подключить существующую таблицу как секцию
ALTER TABLE orders ATTACH PARTITION orders_2026_04
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
DROP TABLE на секции — мгновенная операция, в отличие от DELETE FROM orders WHERE created_at < '2025-01-01' которое может работать часами.
Автоматическое создание секций
В реальных проектах секции создаются заранее (через pg_cron или скрипт деплоя):
-- Создать секцию на следующий месяц
DO $$
DECLARE
next_month DATE := DATE_TRUNC('month', NOW() + INTERVAL '1 month');
partition_name TEXT := 'orders_' || TO_CHAR(next_month, 'YYYY_MM');
BEGIN
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF orders
FOR VALUES FROM (%L) TO (%L)',
partition_name,
next_month,
next_month + INTERVAL '1 month'
);
END $$;
Подводные камни
Партиционирование не заменяет индексы. Partition pruning работает только по ключу партиционирования. По остальным полям всё равно нужны индексы.
Ограничения уникальности. UNIQUE и PRIMARY KEY должны включать ключ партиционирования — иначе PostgreSQL физически не может проверить уникальность между секциями.
Миграция существующей таблицы. Нельзя превратить обычную таблицу в секционированную напрямую. Нужно: создать новую секционированную таблицу → скопировать данные → переименовать.
Итог
| Тип | Когда использовать |
|---|---|
RANGE | Временные ряды, даты, числовые диапазоны |
LIST | Регион, статус, категория (дискретные значения) |
HASH | Равномерное распределение без естественного ключа |
Секционирование — инструмент для больших таблиц. Для таблиц до нескольких ГБ индексы решают задачу эффективнее.
Проверь знания по оптимизации PostgreSQL в нашем тренажёре.