Хорошо спроектированная схема partitioning — половина дела. Вторая половина — операционная: новую partition нужно создавать заранее (иначе INSERT падает или попадает в DEFAULT), старую — удалять или архивировать, и всё это делать на работающем продакшене, где ACCESS EXCLUSIVE lock на parent даже на секунду означает 5xx ошибки у пользователей. В этом уроке разбираем, как добавлять и удалять partitions безопасно, и зачем существует pg_partman.
Жизненный цикл partition
В типичной time-series системе partition проходит четыре фазы:
От создания до архива. На каждом переходе важно не блокировать parent: иначе писатели и читатели встанут.
Ключевая мысль: каждая partition — это обычная таблица. Её можно делать read-only, делать на ней VACUUM FREEZE (чтобы освободить от MVCC tracking), переносить в другую tablespace на медленные диски, dump’ить через pg_dump и наконец дропать.
CREATE PARTITION: подготовка заранее
Сначала простой вариант — создаём partition за день до начала её диапазона.
Создаём parent и одну месячную partition. Затем готовим следующую заранее.
Если забыть подготовить февраль и INSERT придёт с occurred_at = '2024-02-01' — Postgres вернёт ошибку no partition of relation "events" found for row (если нет DEFAULT-partition), либо строка попадёт в DEFAULT. Оба варианта плохие: первый — выпадение бизнеса, второй — DEFAULT раздувается и теряет ценность partitioning. Поэтому autocreate новой partition заранее — обязательная часть операций.
ATTACH PARTITION: миграция существующей таблицы
Когда нужно вкатить partitioning на уже работающую таблицу с историей — типичный приём такой:
- Создаётся новая partitioned table
orders_new. - Создаются partitions, в которые наливаются данные из старой
orders. - Старая
ordersпереименовывается вorders_2023_archiveи прикрепляется как partition. orders_newпереименовывается вorders.
Шаг 3 — это ALTER TABLE orders_new ATTACH PARTITION orders_2023_archive FOR VALUES FROM (...) TO (...). Postgres при ATTACH проверяет, что все строки прикрепляемой таблицы удовлетворяют диапазону — то есть сканирует таблицу целиком.
Прикрепляем существующую таблицу как partition. Postgres сканирует её при ATTACH для проверки bounds.
Чтобы избежать полного сканирования, можно заранее добавить на старую таблицу совпадающий CHECK-констрейнт. Postgres увидит, что constraint уже гарантирует bounds, и пропустит сканирование:
ALTER TABLE orders_old
ADD CONSTRAINT orders_old_dates_check
CHECK (placed_at >= '2023-01-01' AND placed_at < '2024-01-01') NOT VALID;
ALTER TABLE orders_old VALIDATE CONSTRAINT orders_old_dates_check;
-- ^ это идёт в режиме SHARE UPDATE EXCLUSIVE — параллельные INSERT/UPDATE работают
ALTER TABLE orders ATTACH PARTITION orders_old
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');
-- ^ ATTACH быстрый, так как полный scan уже не нужен
Это критически важный приём для миграции продакшена: разница между минутным даунтаймом (со сканированием) и миллисекундным (без него).
DETACH PARTITION: проблема ACCESS EXCLUSIVE
Удалить старую partition можно так:
ALTER TABLE orders DETACH PARTITION orders_2023_01;
DROP TABLE orders_2023_01;
Это работает, но DETACH берёт ACCESS EXCLUSIVE lock на parent. На время этого DDL приложение не может выполнить никаких операций к orders — даже SELECT. На больших таблицах с активной нагрузкой это секунды-десятки секунд недоступности.
Postgres 14 принёс
DETACH PARTITION CONCURRENTLYОбычный DETACH блокирует parent на ACCESS EXCLUSIVE; CONCURRENTLY работает в две фазы без блокировки writers/readers.
Создаём parent с 3 partition и пробуем DETACH. Pglite поддерживает синтаксис; смотрим на финальное состояние.
В продакшене с Postgres 14+ ту же команду пиши как ALTER TABLE events DETACH PARTITION events_2024_01 CONCURRENTLY — приложение не заметит. Pglite в браузере не имеет настоящей конкурентности, но синтаксис поддерживает.
Удаление: DROP TABLE vs DELETE
После DETACH партиция превращается в обычную таблицу. Её можно:
DROP TABLE— мгновенно, без WAL на каждой строке. Это главный аргумент за partitioning: удаление 50 миллионов старых строк черезDELETE FROM orders WHERE placed_at < '2023-01-01'— это часы, гигабайты WAL и риск replication lag. ЧерезDROP TABLE orders_2023_01_archive— миллисекунды.TRUNCATE— очистить содержимое, оставить таблицу.- Архив:
pg_dump --table=orders_2023_01_archiveв файл, потом DROP. Стандартный паттерн для cold storage.
pg_partman: автоматизация
Писать руками CREATE TABLE orders_2024_03 PARTITION OF orders FOR VALUES ... 12 раз в год скучно и опасно (забудешь — приложение упадёт). В продакшене это автоматизируют. Стандартное решение —
- автоматического создания новых partition «вперёд» (например, на 6 месяцев) — по расписанию через cron;
- retention policy — автоматический DETACH старых partition (опционально с переносом в архивную схему);
- статистики и логирования operations.
Pglite не имеет pg_partman, поэтому покажем только концептуально, как это выглядит в продакшене:
-- 1. Установка extension
CREATE EXTENSION pg_partman;
-- 2. Регистрация partitioned table
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'occurred_at',
p_type := 'native',
p_interval := 'monthly',
p_premake := 6 -- сразу подготовить 6 partition вперёд
);
-- 3. Retention: дропать partitions старше 24 месяцев
UPDATE partman.part_config
SET retention = '24 months',
retention_keep_table = false, -- DROP вместо DETACH
retention_keep_index = false
WHERE parent_table = 'public.events';
-- 4. Cron-задача: каждый час
SELECT cron.schedule('partman_maintenance', '0 * * * *', $$
SELECT partman.run_maintenance(p_analyze := true);
$$);
Альтернативы pg_partman:
Что не делает partitioning
Партиционирование часто рассматривают как «способ ускорить запросы». На самом деле его реальная польза не в этом:
- Удаление старых данных через DROP TABLE — самый ценный эффект. На write-heavy системах это спасает от deletion-induced bloat.
- Параллельные VACUUM/REINDEX — autovacuum обрабатывает каждую partition независимо. На монолитной таблице 500 GiB VACUUM крутится сутки и блокирует statistics.
- Tablespaces для разных partition — горячие данные на NVMe, холодные на медленных дисках.
- Pruning ускоряет запросы только при правильно выбранном partition key. Если запросов по диапазону этого ключа мало — partitioning только добавляет overhead.
И есть цены:
- Каждая partition — отдельный heap, отдельные индексы. Памяти
shared_buffersнужно больше. - Planner становится медленнее: на 1000 partition даже простой запрос планируется заметно дольше.
- Глобальных unique-индексов нет (PK должен включать partition key).
- Foreign keys ограничены.
Чек-лист
- Готовь partitions заранее — за дни до начала диапазона, иначе INSERT упадёт или попадёт в DEFAULT.
- ATTACH PARTITION сканирует таблицу для проверки bounds; добавь совпадающий CHECK заранее, чтобы пропустить sсan.
- DETACH PARTITION берёт ACCESS EXCLUSIVE на parent. В Postgres 14+ используй
DETACH PARTITION ... CONCURRENTLY. - DROP TABLE old_partition мгновенно удаляет 100 миллионов строк без WAL — главное преимущество partitioning.
- pg_partman автоматизирует create/retention partitions; запускается через pg_cron или внешний scheduler.
- Partitioning не ускоряет запросы по умолчанию — оно даёт инструмент, который надо использовать (pruning, parallel VACUUM, DROP вместо DELETE).
- Цена: больше памяти, медленнее planner, нет глобальных unique-индексов, ограниченные FK.