Learning Platform
Урок 13.04 · 22 мин
Продвинутый
ATTACH PARTITIONDETACH PARTITIONCONCURRENTLYpg_partmanOperations

Хорошо спроектированная схема partitioning — половина дела. Вторая половина — операционная: новую partition нужно создавать заранее (иначе INSERT падает или попадает в DEFAULT), старую — удалять или архивировать, и всё это делать на работающем продакшене, где ACCESS EXCLUSIVE lock на parent даже на секунду означает 5xx ошибки у пользователей. В этом уроке разбираем, как добавлять и удалять partitions безопасно, и зачем существует pg_partman.

Жизненный цикл partition

В типичной time-series системе partition проходит четыре фазы:

Жизненный цикл monthly partition

От создания до архива. На каждом переходе важно не блокировать parent: иначе писатели и читатели встанут.

ПодготовкаCREATE TABLE ... PARTITION OF
за 1-2 днядо начала диапазона
Активнаяпринимает INSERT/UPDATE
N дней/месяцевhot path
Read-onlySET read_only = on; VACUUM FREEZE
cold tierбез писаний
Архив или DROPDETACH CONCURRENTLY -> dump -> DROP TABLE
retention policyконец жизни

Ключевая мысль: каждая partition — это обычная таблица. Её можно делать read-only, делать на ней VACUUM FREEZE (чтобы освободить от MVCC tracking), переносить в другую tablespace на медленные диски, dump’ить через pg_dump и наконец дропать.

CREATE PARTITION: подготовка заранее

Сначала простой вариант — создаём partition за день до начала её диапазона.

Создаём parent и одну месячную partition. Затем готовим следующую заранее.

PostgreSQL

Если забыть подготовить февраль и 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 на уже работающую таблицу с историей — типичный приём такой:

  1. Создаётся новая partitioned table orders_new.
  2. Создаются partitions, в которые наливаются данные из старой orders.
  3. Старая orders переименовывается в orders_2023_archive и прикрепляется как partition.
  4. orders_new переименовывается в orders.

Шаг 3 — это ALTER TABLE orders_new ATTACH PARTITION orders_2023_archive FOR VALUES FROM (...) TO (...). Postgres при ATTACH проверяет, что все строки прикрепляемой таблицы удовлетворяют диапазону — то есть сканирует таблицу целиком.

Прикрепляем существующую таблицу как partition. Postgres сканирует её при ATTACH для проверки bounds.

PostgreSQL

Чтобы избежать полного сканирования, можно заранее добавить на старую таблицу совпадающий 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 vs DETACH CONCURRENTLY

Обычный DETACH блокирует parent на ACCESS EXCLUSIVE; CONCURRENTLY работает в две фазы без блокировки writers/readers.

DETACH PARTITIONнаивный
LockACCESS EXCLUSIVE on parent
Длительностьзависит от размера каталога
Эффектвсё приложение замерло
DETACH ... CONCURRENTLYPostgres 14+
LockSHARE UPDATE EXCLUSIVE
Фаза 1отметить pending, дождаться tx
Фаза 2финальный DETACH мгновенно
Эффектwriters/readers продолжают
Ограничения CONCURRENTLYне в транзакции; не для DEFAULT partition; недопустим вложенный CONCURRENTLY на той же таблице

Создаём parent с 3 partition и пробуем DETACH. Pglite поддерживает синтаксис; смотрим на финальное состояние.

PostgreSQL

В продакшене с 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 раз в год скучно и опасно (забудешь — приложение упадёт). В продакшене это автоматизируют. Стандартное решение —

pg_partman
— extension, добавляющая функции для:

  • автоматического создания новых 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:

pg_cron
+ кастомный SQL-скрипт, который crontab вызывает раз в день. Получается проще, но без logging/retry. Для серьёзных систем — pg_partman.

Что не делает partitioning

Партиционирование часто рассматривают как «способ ускорить запросы». На самом деле его реальная польза не в этом:

  1. Удаление старых данных через DROP TABLE — самый ценный эффект. На write-heavy системах это спасает от deletion-induced bloat.
  2. Параллельные VACUUM/REINDEX — autovacuum обрабатывает каждую partition независимо. На монолитной таблице 500 GiB VACUUM крутится сутки и блокирует statistics.
  3. Tablespaces для разных partition — горячие данные на NVMe, холодные на медленных дисках.
  4. Pruning ускоряет запросы только при правильно выбранном partition key. Если запросов по диапазону этого ключа мало — partitioning только добавляет overhead.

И есть цены:

  • Каждая partition — отдельный heap, отдельные индексы. Памяти shared_buffers нужно больше.
  • Planner становится медленнее: на 1000 partition даже простой запрос планируется заметно дольше.
  • Глобальных unique-индексов нет (PK должен включать partition key).
  • Foreign keys ограничены.
Проверка знанийKnowledge check
У тебя monthly partitioned events, 36 partition (3 года). Retention — 24 месяца. Каждое первое число месяца нужно дропать самую старую partition и создавать новую вперёд. Что произойдёт, если просто запустить ALTER TABLE events DROP PARTITION events_2022_05 в Postgres 13 на нагруженной системе?
ОтветAnswer
Главное: команды DROP PARTITION в Postgres нет — это синтаксис Oracle. Нужно сделать DETACH + DROP TABLE. Если запустить ALTER TABLE events DETACH PARTITION events_2022_05 в Postgres 13 — это возьмёт ACCESS EXCLUSIVE lock на parent events. Все читатели и писатели events заблокируются до завершения. Длительность DDL зависит от внутренних операций над каталогом (обычно секунды, но под нагрузкой бывает дольше) — это окно даунтайма. С Postgres 14+ решение — DETACH PARTITION CONCURRENTLY, которое работает в две фазы без блокировки writers. После DETACH partition превращается в обычную таблицу, и её можно DROP TABLE мгновенно — без блокировки parent. В продакшене всю эту цепочку обычно автоматизирует pg_partman через retention policy.

Чек-лист

  • Готовь 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.
Time Travel, Checkpoints и VACUUM в Delta Lake fsync и durability — когда write на самом деле записан на диск

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какое преимущество DETACH PARTITION CONCURRENTLY (Postgres 14+) даёт перед обычным DETACH PARTITION?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5