Перейти к содержанию

Политика хранения партиций — это не DROP TABLE

Задача очистки — та строка настройки партиционирования, которую никто не проверяет: найти старые партиции, удалить, запускать каждую ночь. Я дал ей таблицу с пятнадцатью месячными партициями, ещё одной присоединённой вручную и второй таблицей с внешним ключом на данные. Задача удалила чужую партицию, а обходя отказ PostgreSQL в DROP, снесла все семнадцать внешних ключей таблицы, которую вообще не должна была трогать.

Числа получены в эксперименте статьи с PostgreSQL 17 в контейнере. Версии: pg-partsmith 1.5.0, Python 3.13.

Задача, которую пишут все

Перечислить дочерние таблицы, отсортировать по имени, оставить двенадцать новых, удалить остальные.

    the job's list: ['events__2025_07', 'events__2025_08', 'events__2025_09', 'events_archive_old']
    foreign keys on receipts before the job: 17
    DROP TABLE events__2025_07: DependentObjectsStillExistError: cannot drop table events__2025_07
      because other objects depend on it
    DROP TABLE events__2025_07 CASCADE: dropped
    DROP TABLE events__2025_08: dropped
    DROP TABLE events__2025_09: dropped
    DROP TABLE events_archive_old: dropped
    foreign keys on receipts after the job:  0

В девяти строках случились четыре ошибки.

Удалена чужая таблица. events_archive_old присоединили вручную с диапазоном вне месячной схемы и намеренно помещёнными туда данными. Она попала в список как дочерняя таблица, а её имя не входило в двенадцать самых новых. Шаблон имени не доказывает принадлежность.

PostgreSQL правильно отказал. Нельзя удалить присоединённую партицию, на которую указывает внешний ключ: ограничение зависит от таблицы. Отказ защищает ссылающиеся строки.

Задача обошла этот отказ. CASCADE предлагает сообщение ошибки и добавляет следующий коммит. Удаление проходит, снося зависимые объекты: исчезают все семнадцать внешних ключей receipts, по одному на партицию плюс родитель. Гарантия, что квитанция ссылается на реальное событие, пропадает в таблице вне полномочий задачи — без ошибки и записи в её журнале.

Данные удалились сразу. Удаление присоединённой партиции уничтожает строки. Между «эта партиция больше не обслуживается» и «этой партиции больше нет» не осталось окна, чтобы заметить неверный срок хранения до потери данных.

Последний пункт выражает общую проблему: DROP — один немедленный необратимый шаг. Политике хранения нужна последовательность, и главное проектирование — между её шагами.

Та же очистка, выраженная политикой и запрошенная как план:

    plan for public.events at 2026-09-07T11:17:38+00:00
      DETACH public.events__2025_07 (retention_expired)
      DETACH public.events__2025_08 (retention_expired)
      DETACH public.events__2025_09 (retention_expired)
      DROP public.events__2025_07 (follows_detach)
      DROP public.events__2025_08 (follows_detach)
      DROP public.events__2025_09 (follows_detach)
      [info] unmanaged_partition: public.events_archive_old covers 2025-05-01 .. 2025-07-01, which is
      not a window of the configured scheme; it is not a lifecycle partition and is left alone.
    nothing has run yet: 16 attached, 0 detached, 301 rows through the parent

Это даёт две разные возможности.

Первая: разрушительную операцию можно проверить. У неё есть имя, код причины и место в списке для ревью, dry-run в CI или просмотра в три часа ночи перед запуском. На вопрос «что удалится сегодня?» появляется ответ.

Вторая — строка [info]. Ручная партиция отражена в диагностике: планировщик увидел её, понял, что границы не соответствуют сетке жизненного цикла, и сообщил об этом. Принадлежность определяется границами, а не именем. Несоответствующее остаётся нетронутым и показывается человеку. Молчаливый пропуск тоже плох: вы так и не узнаете о таблице внутри дерева, которая не обслуживается.

Отсоединение и удаление — два шага

Отсоединение убирает партицию из родителя: запросы через events больше её не видят, запись туда не направляется, но таблица со строками остаётся. Удаление уничтожает её. Между этими действиями ошибку ещё можно исправить.

    detached=2 dropped=0 issues=1
    after the tick with a 7-day grace: 14 attached, 2 detached, 261 rows through the parent
      detached: events__2025_07, events__2025_09

При отсрочке в семь дней ночной запуск только отсоединяет. Если срок хранения оказался неверным, достаточно присоединить таблицу с сохранившимися данными вместо восстановления из резервной копии. Через семь дней, если проблем нет, она удаляется.

Во многих инструментах, и здесь без явной настройки, отсрочка нулевая: отсоединение и удаление происходят за один запуск. Для неважных данных это допустимо, для остальных — плохой выбор. Решите явно: цена отсрочки — место на диске, цена её отсутствия — восстановление.

Архивирование выполняется до удаления и может его запретить

Обычно хранение означает «это больше не нужно в горячей таблице», а не «никогда не понадобится». Поэтому до удаления вызывается обработчик архивации. Главное — последствия его ошибки:

    detached=0 dropped=0 issues=0 error='RuntimeError: the archive bucket is not reachable'
    after the tick whose hook raised: 14 attached, 2 detached, 261 rows through the parent
    the next tick, hook working: dropped=2, archived=['public.events__2025_07', 'public.events__2025_09']

Обработчик выбросил исключение, ничего не удалилось, запуск сообщил об ошибке. Партиции остались на месте. Следующий запуск при доступном архиве сохранил и удалил их. Ошибка before-drop должна отменять drop; иначе архивация превращается в необязательное действие, при неудаче которого данные всё равно исчезают.

Отдельная задача архивации перед задачей очистки оставляет гонку, заметную под нагрузкой: очистка не знает, закончился ли экспорт. Когда архивация — часть удаления, вопрос исчезает.

ИДЕЯ В СХЕМЕОтсоединение и удаление — отдельные шаги
---
config:
  theme: default
  look: classic
  flowchart:
    useMaxWidth: false
    wrappingWidth: 150
    padding: 12
    nodeSpacing: 24
    rankSpacing: 32
---
flowchart TD
    accTitle: Отсоединение и удаление — отдельные шаги
    accDescr: Истёкшие партиции отсоединяются до удаления. До DROP должны пройти период ожидания и успешная архивация; ошибка архиватора должна сохранить отсоединённые данные.
 E["Партиция подходит для очистки"] --> D["DETACH"] --> G["Период ожидания"] --> A["Архиватор"] --> Q{"Успешно?"}
 Q -->|"Да"| X["DROP"]
 Q -->|"Нет"| K["Сохранить данные; сообщить ошибку"]

Истёкшие партиции отсоединяются до удаления. До DROP должны пройти период ожидания и успешная архивация; ошибка архиватора должна сохранить отсоединённые данные.

Как внешний ключ влияет на хранение

Третья партиция в том запуске не была отсоединена:

    issue: detach public.events__2025_08: PartitionReferencedError: Partition public.events__2025_08 is
    still referenced by rows of another table: removing partition "events__2025_08" violates foreign
    key constraint "receipts_event_id_event_at_fkey2"

PostgreSQL не даёт отсоединить партицию со строками, на которые ещё ссылаются. Запуск записал проблему и продолжил с двумя другими. Это безопасно, но шумно: сообщение будет повторяться каждую ночь, пока ссылки не исчезнут. Добавление CASCADE из первого раздела — неправильное «исправление» этого шума.

Правильное решение — включить условие в политику, чтобы партиция со ссылками вообще не считалась просроченной:

    with Unreferenced() in the policy, the plan is: nothing to do
    after the referencing row is gone: detached=1 dropped=1

«Старше двенадцати новых месяцев и больше никем не используется». Пока существует квитанция, план пуст, лишней диагностики нет. Партиция выходит из обращения в первый запуск после исчезновения последней ссылки. Политика описывает допустимое удаление, а не воюет с БД по расписанию.

Проверка задачи очистки

  • Подтверждать принадлежность до разрушительных действий. Границы на сетке схемы или маркер самого инструмента; не шаблон имени.
  • Иметь печатаемый план. Если нельзя узнать, что удалится ночью, нельзя проверить изменение.
  • Разделять detach и drop достаточной для обнаружения ошибки отсрочкой.
  • Никогда не использовать CASCADE. Он превращает защитный отказ в повреждение связанных объектов другой таблицы.
  • Архивировать в before-drop, где ошибка останавливает удаление.
  • Учитывать ссылающиеся строки в политике, а не только в журнале ошибок.
  • Запускать dry-run в CI. План для года партиций занимает несколько строк; их diff выявляет изменение срока хранения до выполнения.

Инструменты

Всё выше предоставляет pg-partsmith: план с причинами и диагностикой, принадлежность по границам и маркер до отсоединения, отсрочка удаления, обработчики каждой фазы и предикат Unreferenced(). Библиотека не выполняет CASCADE и не удаляет присоединённые партиции, поэтому первый сценарий ей недоступен.

Семнадцать внешних ключей чужой для задачи таблицы удалены строкой, предназначенной экономить диск.