PostgreSQL 18: почему ATTACH PARTITION тормозит запись и как убрать сканы через CHECK
Скрипт ротации месячных секций десятки раз завершался за секунды, а однажды простоял двадцать шесть минут и остановил запись событий. Причина оказалась не в размере новой таблицы, а в живом DEFAULT-разделе, очереди на ACCESS EXCLUSIVE и ограничениях, которым PostgreSQL не мог доверять. Ниже я разберу реальные блокировки ATTACH PARTITION в PostgreSQL 18, покажу корректные CHECK для новой и DEFAULT-секции и соберу регламент, при котором операция быстро завершается либо так же быстро отступает по lock_timeout.
Какие блокировки действительно берёт ATTACH PARTITION
Фраза «ATTACH почти не блокирует» появилась не на пустом месте. В руководстве PostgreSQL сказано, что ALTER TABLE ... ATTACH PARTITION запрашивает на родительской секционированной таблице SHARE UPDATE EXCLUSIVE, тогда как CREATE TABLE ... PARTITION OF требует ACCESS EXCLUSIVE. SHARE UPDATE EXCLUSIVE совместима с обычными ACCESS SHARE и ROW EXCLUSIVE, поэтому чтение и стандартный DML по родителю сами по себе не должны останавливаться. Но родитель — только одна из целей блокировки.
Полный перечень находится в описании ALTER TABLE. Помимо SHARE UPDATE EXCLUSIVE на родителе, сервер запрашивает ACCESS EXCLUSIVE на присоединяемой таблице и на DEFAULT-разделе, если тот существует. Блокировка присоединяемой таблицы обычно безобидна, когда это заранее созданная и никому пока не доступная заготовка. ACCESS EXCLUSIVE на рабочем DEFAULT-разделе гораздо опаснее: она конфликтует со всеми обычными обращениями именно к этому разделу.
Важное уточнение: подходящий CHECK убирает сканирование, но не отменяет сам запрос ACCESS EXCLUSIVE на DEFAULT-раздел. Поэтому даже при идеально подготовленных ограничениях ATTACH может ждать долгую транзакцию, ручной VACUUM или autovacuum, защищающий таблицу от wraparound. Разница в том, что после получения блокировок операция без сканов завершается быстро и не удерживает их во время чтения миллионов строк.
Без доказательства границ PostgreSQL проводит две отдельные проверки. Присоединяемую обычную таблицу сканируют под ACCESS EXCLUSIVE, чтобы найти строки вне объявленного диапазона. DEFAULT-раздел сканируют под собственной ACCESS EXCLUSIVE, чтобы убедиться, что там нет строк новой секции. Если присоединяемая таблица или DEFAULT сами секционированы, дополнительные блокировки и проверки распространяются вниз по иерархии, пока сервер не встретит достаточное ограничение или листовую таблицу.
Валидный CHECK здесь нужен не планировщику запроса, а механизму проверки ограничений во время DDL. PostgreSQL должен суметь логически вывести из выражения CHECK требуемую границу. Текст не обязан совпадать посимвольно с FOR VALUES, но простые одинаково типизированные сравнения надёжнее сложных функций и преобразований. В PostgreSQL 18 также нельзя рассчитывать на CHECK с convalidated = false или conenforced = false: такой объект не доказывает, что все старые строки соответствуют условию.
- SHARE UPDATE EXCLUSIVE — родительская таблица; обычные SELECT и DML совместимы с этим режимом
- ACCESS EXCLUSIVE — присоединяемая таблица; без подходящего CHECK возможен её полный скан
- ACCESS EXCLUSIVE — DEFAULT-раздел; блокировка запрашивается даже тогда, когда CHECK позволяет пропустить скан
- дополнительные блокировки — дочерние таблицы присоединяемой или DEFAULT-секции при многоуровневом секционировании
- валидность проверяется по `pg_constraint.convalidated`, а в PostgreSQL 18 следует учитывать и `pg_constraint.conenforced`
Разбор инцидента: 1,4 млрд строк и 26 минут ожидания
Условный клиент в этом разборе — ветеринарная сеть «Айболит-Вет», 5 клиник, 41 РМ. Стенд работал на PostgreSQL 18: виртуальная машина с 8 vCPU, 32 ГБ RAM и NVMe-хранилищем. Таблица events принимала события лабораторного оборудования и контроллеров, была разбита по RANGE-ключу ts типа timestamptz на месячные секции и хранила данные за 24 месяца. На момент разбора в ней было около 1,4 млрд строк, 26 активных секций и примерно 780 ГБ данных.
Рядом существовал events_default, заведённый как страховка от отсутствующей секции. Ночной скрипт первого числа создавал очередную events_YYYY_MM и выполнял ATTACH. В проблемную ночь команда стартовала в 03:00:04, но к 03:26 ещё оставалась активной. В pg_stat_activity накопилось 340 клиентских сессий с wait_event_type = 'Lock'. Это не означало, что остановился весь кластер: встала запись текущих событий, которые до появления новой месячной секции маршрутизировались именно в DEFAULT.
В events_default обнаружилось 4,2 млн строк общим размером около 3,1 ГБ. Они копились 11 месяцев. Восемь контроллеров работали без нормальной синхронизации времени: часть событий имела отметку 1970 года, часть уходила вперёд на 18 месяцев. В итоге DEFAULT содержал не только заведомо старый мусор, но и даты будущих месячных диапазонов. Когда очередь дошла до месяца, уже представленного такими строками, ATTACH должен был проверить заведомо конфликтующее содержимое.
Вторым участником инцидента оказался autovacuum по events_default, запущенный для предотвращения wraparound. Это принципиальная деталь. Обычный autovacuum PostgreSQL, как правило, прерывает при появлении конфликтующей DDL-блокировки. Процесс, защищающий от wraparound, автоматически не прерывается; в pg_stat_activity его запрос заканчивается пометкой (to prevent wraparound). Поэтому правдоподобное длительное ожидание нельзя объяснять просто словами «в это время шёл любой autovacuum».
Около 19 минут ATTACH ждал ACCESS EXCLUSIVE. Новые события текущего месяца продолжали направляться в events_default и вставали за ожидающей блокировкой, поэтому очередь быстро росла. Получив блокировку, PostgreSQL примерно 7 минут читал 3,1 ГБ и проверял предикат на фоне нагрузки от репликации и дисковых операций. Затем команда не могла успешно присоединить секцию, потому что нашла строки её диапазона в DEFAULT. Общий ущерб составили те же 26 минут: большая часть ушла на ожидание, меньшая — на скан, а результатом стала ошибка ограничения.
Из этого случая я вынес два разных правила. Первое: пустота DEFAULT должна контролироваться до начала DDL, а не выясняться сканированием внутри ATTACH. Второе: даже корректный CHECK не гарантирует мгновенное получение ACCESS EXCLUSIVE, поэтому команда не должна бесконечно стоять в очереди. Если блокировка не выдана за несколько секунд, безопаснее отменить попытку и повторить её позже.
- `events` — 1,4 млрд строк, около 780 ГБ, RANGE по `ts`, 26 активных секций
- `events_default` — 4,2 млн строк и около 3,1 ГБ, накопленных за 11 месяцев
- источник некорректных дат — 8 контроллеров: отметки 1970 года и смещение до 18 месяцев вперёд
- ожидание блокировки — около 19 минут; проверка DEFAULT — ещё около 7 минут
- пострадала запись текущих событий через DEFAULT: 340 сессий ожидали блокировки
CHECK на присоединяемой таблице: убираем первый скан
Для RANGE-секции нижняя граница включительна, а верхняя исключительна. Поэтому месячный CHECK записываю через >= и <. BETWEEN здесь неудобен: он включает обе границы и при соседних месяцах разрешил бы значение, принадлежащее следующей секции. Константы для timestamptz я типизирую явно и указываю смещение, чтобы результат не зависел от TimeZone сессии, запускающей DDL.
Заготовку удобнее создавать обычной таблицей через LIKE, а не сразу через PARTITION OF. В примере используется INCLUDING ALL: в PostgreSQL 18 это сокращение для копирования всех поддерживаемых свойств, включая CHECK, defaults, generated- и identity-описания, параметры хранения, индексы и ограничения. Для реальной схемы я всё равно сравниваю получившиеся индексы, владельца, права и поведение identity-последовательностей: LIKE создаёт независимую таблицу, а для скопированной identity-колонки — отдельную последовательность.
Если таблица пустая, обычный ADD CONSTRAINT проверит её почти мгновенно. Для наполненной архивной таблицы этот же шаг потребует скан под ACCESS EXCLUSIVE. Тогда я сначала добавляю CHECK как NOT VALID, фиксирую короткую транзакцию, а затем отдельно выполняю VALIDATE CONSTRAINT. Валидация читает старые строки под SHARE UPDATE EXCLUSIVE и совместима с обычными изменениями данных, хотя конкурирующая DDL всё равно может ждать.
Само наличие ограничения ещё ничего не гарантирует. После валидации проверяю convalidated = true; для PostgreSQL 18 также оставляю ограничение в состоянии ENFORCED, то есть conenforced = true. Выражение должно логически разрешать только будущий диапазон секции. Посимвольное равенство не требуется, но я генерирую CHECK и FOR VALUES из одних параметров, чтобы не получить расхождение типов, часового пояса или верхней даты.
Для LIST-секции, которая не принимает NULL, документация требует дополнительно установить NOT NULL на колонку ключа, если ключ не является выражением. PostgreSQL 18 хранит NOT NULL в pg_constraint и позволяет добавлять именованный NOT NULL как NOT VALID, а затем валидировать его. Корректная форма — ALTER TABLE ... ADD CONSTRAINT имя NOT NULL колонка NOT VALID, а не выдуманный флаг после определения колонки. Для ключа-выражения действует отдельное ограничение: при секции, не принимающей NULL, CHECK-трюк может не избавить от скана.
После успешного ATTACH дополнительный CHECK границ можно удалить: встроенное ограничение секции уже обеспечивает тот же диапазон. Я делаю DROP отдельной короткой командой после присоединения, потому что блокировки DDL живут до конца транзакции. Для часто используемой секции удаление дублирующей проверки также избавляет каждую последующую вставку от лишнего вычисления.
- RANGE: нижняя граница включительна, верхняя исключительна — используйте `>=` и `<`
- пустая заготовка — сразу валидный CHECK; наполненная — `NOT VALID`, затем `VALIDATE CONSTRAINT`
- доверенный CHECK — `convalidated = true` и `conenforced = true`
- `INCLUDING ALL` копирует индексы, но результат и identity-последовательности нужно проверить до ATTACH
- после успешного ATTACH дополнительный CHECK границ можно удалить отдельной короткой командой
DEFAULT-раздел: годовой CHECK без логической ошибки
DEFAULT — главный источник длинного ATTACH. Когда добавляется обычная секция, PostgreSQL изменяет внутреннее ограничение DEFAULT: теперь этот раздел должен исключать и новый диапазон. Если сервер не может доказать отсутствие нужных строк по существующему CHECK, он читает DEFAULT под ACCESS EXCLUSIVE. Решение принимается по ограничениям, а не по результату вчерашнего count(*). Даже логически пустая, но раздутая таблица может заставить сервер просмотреть множество физических страниц.
Если в DEFAULT уже есть хотя бы одна строка нового диапазона, ATTACH закончится ошибкой о нарушении обновлённого ограничения DEFAULT. Такой набор данных сначала надо разобрать: исправить ошибочные даты, создать целевые секции безопасным способом и перенести данные порциями. Попытка многократно перезапускать тот же ATTACH ничего не исправляет и лишь повторяет блокировку и скан.
В исходном варианте регламента была логическая неточность: CHECK вида ts < начало_2026 исключает не только 2026 год, но и всё последующее время. Такое ограничение не надо ежегодно заменять, однако оно навсегда запрещает DEFAULT принимать будущие даты. Если нужен ежегодный компромисс, выражение должно исключать ровно один год: дата меньше его начала либо не меньше начала следующего года.
Ограничение для конкретного года позволяет без скана DEFAULT присоединять все двенадцать месячных секций этого года. Но краткая ACCESS EXCLUSIVE на DEFAULT всё равно запрашивается. Перед следующим годом я сначала добавляю новое ограничение как NOT VALID, валидирую его и только потом удаляю старое. ADD CONSTRAINT ... NOT VALID и DROP всё равно требуют коротких сильных блокировок, тогда как длительный VALIDATE CONSTRAINT использует SHARE UPDATE EXCLUSIVE.
Такой CHECK сознательно меняет поведение аварийного маршрута. Если секция за октябрь ещё не создана, строка с октябрьской датой будет направлена в DEFAULT, после чего нарушит events_default_no_2026. При полном отсутствии DEFAULT сервер вместо этого сообщил бы, что подходящая секция не найдена. Я предпочитаю явную ошибку и ретрай молчаливому накоплению данных, но для систем без повторной доставки это решение надо согласовать с владельцем приложения.
Есть ещё одно ограничение, которое нельзя спрятать за CHECK: ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY запрещён, если у родительской таблицы существует DEFAULT-раздел. Для таблиц с регулярным ретеншеном это весомый аргумент отказаться от DEFAULT полностью. Обычный DETACH остаётся доступен, но требует более сильной блокировки родителя.
- подходящий CHECK исключает скан DEFAULT, но не запрос ACCESS EXCLUSIVE на него
- строки нового диапазона в DEFAULT приводят к ошибке ATTACH, а не просто к задержке
- условие `ts < начало_года` исключает этот год и всё будущее; ежегодным оно не является
- для исключения ровно одного года нужен дизъюнктивный диапазон с `OR`
- при наличии DEFAULT форма `DETACH PARTITION CONCURRENTLY` запрещена
Регламент ротации: premake, тайм-ауты и статистика
Ротацию я разделяю на подготовку и короткое изменение дерева секций. Заготовки создаются не в первые часы нового месяца, а заранее. Для большинства рабочих схем начинаю с горизонта от трёх до шести месяцев, после чего корректирую его по максимальному допустимому смещению входных дат. Это эксплуатационная рекомендация, а не предел PostgreSQL. В случае сети «Айболит-Вет» горизонт должен был перекрывать известные отклонения оборудования либо кривые будущие даты следовало отбрасывать ещё на приёме.
Premake решает проблему на уровне маршрутизации: корректная будущая строка сразу попадает в готовую секцию и не загрязняет DEFAULT. Но один premake не защищает от 1970 года, NULL или даты, улетевшей дальше горизонта. Поэтому рядом нужны метрика непустого DEFAULT, контроль часов источников и отдельный карантин для событий, которые нельзя надёжно исправить автоматически.
На каждую короткую DDL-попытку устанавливаю lock_timeout = '3s'. Это выбранный мной эксплуатационный порог, а не встроенное значение PostgreSQL. Если блокировка не получена, транзакция откатывается, планировщик заданий ждёт минуту и повторяет попытку до двадцати раз. Эти числа не обещают ровно двадцатиминутное окно: продолжительность зависит от времени каждой попытки и пауз. Их надо подбирать под допустимую задержку конкретного сервиса.
Помимо CHECK я готовлю индексы. По правилам ATTACH для каждого индекса родителя PostgreSQL присоединяет валидный эквивалентный индекс новой таблицы либо создаёт соответствующий индекс сам. На пустой секции это обычно быстро; на наполненной архивной таблице неожиданное создание индекса способно сделать ATTACH долгим. Поэтому перед миграцией проверяю эквивалентные индексы заранее и не считаю отсутствие скана данных гарантией мгновенного завершения.
После изменения состава секций проверяю статистику родителя. Autovacuum обрабатывает обычные дочерние секции, но не саму секционированную таблицу и не запускает для неё автоматический ANALYZE. В PostgreSQL 18 появился синтаксис ANALYZE ONLY events: он обрабатывает родительскую секционированную таблицу, не запуская отдельный ANALYZE для каждого потомка. Сбор наследуемой статистики всё равно может получать выборки из секций, поэтому формулировка «вообще не читает дочерние данные» была бы неверной.
Руководство рекомендует повторять ANALYZE, когда распределение данных по секциям заметно изменилось. Я не запускаю его механически после каждого пустого ATTACH, если статистика ещё репрезентативна, но выполняю после крупного переноса, массовой загрузки или существенного DETACH. Это уменьшает риск неоптимальных планов без лишней фоновой работы.
Если таблиц много, вместо самописной автоматики можно использовать pg_partman. На дату проверки актуальный релиз — 5.5.0; его документация описывает premake, retention, check_default() и процедуры переноса данных. У check_default(false) отключается точный подсчёт: функция может быстро сообщить о первой найденной строке. Для PostgreSQL 18 я не ставил бы произвольную старую версию ветки 5.x: релиз 5.5.0 содержит в том числе исправления уязвимостей повышения привилегий и меняет рекомендуемую роль фонового worker на непривилегированную.
- заготовки создаются заранее; горизонт premake выбирается по реальному разбросу входных дат
- `lock_timeout = '3s'` ограничивает ожидание одной DDL-попытки
- до двадцати повторов с минутной паузой — политика примера, а не параметр PostgreSQL
- валидные эквивалентные индексы готовятся до ATTACH наполненной таблицы
- `ANALYZE ONLY events` в PostgreSQL 18 обновляет статистику родителя без отдельного анализа каждой секции
- для нескольких наборов секций подходит актуальный `pg_partman` 5.5.0 с мониторингом DEFAULT
Ошибки, которые превращают короткую DDL в простой
Первая ошибка — использовать CREATE TABLE ... PARTITION OF на нагруженном родителе только ради короткого SQL. Эта форма требует ACCESS EXCLUSIVE на родительской таблице. Такой режим конфликтует и с чтением, и с записью через родителя, поэтому цена удобной одной строки резко возрастает. Связка обычного CREATE TABLE, подготовки объектов и короткого ATTACH оставляет на родителе более слабую SHARE UPDATE EXCLUSIVE.
Вторая ошибка — остановиться на NOT VALID. Такое ограничение уже проверяет новые или изменяемые строки, но не доказывает корректность исторических данных. Пока convalidated = false, ATTACH имеет право выполнить собственную проверку. В PostgreSQL 18 дополнительно появился режим NOT ENFORCED для CHECK, и он тоже не подходит: нужный ограничитель должен быть и валидирован, и принудительно применяться.
Третья ошибка — проверять DEFAULT запросом SELECT count(*) FROM events_default LIMIT 1. LIMIT стоит после агрегирования и не спасает от полного подсчёта строк. Для быстрого сигнала использую SELECT EXISTS (SELECT FROM events_default). Если нужен точный объём проблемы, считаю строки отдельным диагностическим запросом в подходящее время, а не в критической части ротации.
Четвёртая ошибка — считать любой autovacuum причиной многоминутного ожидания. Обычный autovacuum PostgreSQL обычно прерывает при конфликтующей блокировке. Исключение — процесс, предотвращающий wraparound. Кроме того, блокировать могут ручной VACUUM, открытая транзакция приложения или другая DDL. Поэтому я смотрю backend_type, текст запроса, xact_start, pg_locks.granted и реальную цепочку блокировок, а не убиваю первый процесс со словом vacuum.
Пятая ошибка — выполнять ATTACH внутри длинной транзакции вместе с созданием индексов, отчётными запросами и служебными обновлениями. Табличные блокировки обычно удерживаются до COMMIT, а не освобождаются в конце отдельной команды. Даже завершившийся за секунду ATTACH продолжит блокировать DEFAULT, пока приложение занимается остальными действиями.
Шестая ошибка — позволить ATTACH самому создать недостающие индексы на наполненной таблице. Документация прямо говорит, что при отсутствии валидного эквивалентного индекса создаётся новый. Подготовка CHECK устраняет проверочный скан строк, но не стоимость построения индекса. Для архивного присоединения я сначала создаю необходимые индексы, при допустимости использую CREATE INDEX CONCURRENTLY на ещё отдельной таблице, проверяю их валидность и лишь затем запускаю ATTACH.
- `CREATE TABLE ... PARTITION OF` — ACCESS EXCLUSIVE на родителе
- `NOT VALID` без `VALIDATE CONSTRAINT` не избавляет от проверки исторических строк
- CHECK с `conenforced = false` не служит доказательством границ
- `count(*) ... LIMIT 1` не является быстрой проверкой пустоты; используйте `EXISTS`
- обычный autovacuum и autovacuum для предотвращения wraparound ведут себя при конфликтующей DDL по-разному
- все подготовительные операции следует вынести из короткой транзакции ATTACH
Чек-лист перед ATTACH и действия при ожидании
Сначала проверяю DEFAULT быстрым EXISTS, затем просматриваю сами ограничения. Простого convalidated = true недостаточно: выражение на DEFAULT должно действительно исключать присоединяемый диапазон, а CHECK новой таблицы — разрешать только его. Для PostgreSQL 18 проверяю также conenforced. Если используется timestamptz, сравниваю типизированные константы и смещение времени в CHECK и границах секции.
Затем сверяю структуру заготовки с родителем. ATTACH требует одинаковых колонок и типов, а также обязательных NOT NULL и CHECK родителя. Проверяю валидные эквивалентные индексы, потому что отсутствующий индекс будет создан во время ATTACH. Отдельно убеждаюсь, что заготовка не используется приложением: ACCESS EXCLUSIVE на ней неизбежна.
Перед DDL смотрю транзакции и блокировки. Особенно интересуют длительные idle in transaction, ручной VACUUM, другая DDL и autovacuum по events_default. Если процесс предотвращает wraparound, отменять его автоматически опасно; безопаснее отложить ATTACH. Сам ATTACH запускаю в отдельной транзакции с SET LOCAL lock_timeout = '3s', чтобы настройка действовала только до COMMIT или ROLLBACK.
Во время операции держу вторую сессию с pg_stat_activity и pg_locks. Если команда не получила блокировку за ожидаемые несколько секунд, lock_timeout должен завершить попытку сам. При ошибке проверяю, не осталось ли неудачной транзакции в состоянии aborted, и делаю ROLLBACK до повтора. Если тайм-аут забыли установить, нахожу точный PID ATTACH, подтверждаю цепочку и вызываю pg_cancel_backend() для него.
После успеха удаляю дополнительный CHECK с новой секции отдельной короткой командой, проверяю, что секция видна в дереве, и оцениваю необходимость ANALYZE ONLY events. Наконец, снова выполняю быстрый контроль DEFAULT. Такая последовательность отделяет подготовку, способную долго читать данные, от короткой структурной операции и не оставляет результат на веру сообщению cron.
- `SELECT EXISTS (SELECT FROM events_default)` возвращает быстрый признак наличия строк
- CHECK новой таблицы логически соответствует `FOR VALUES FROM/TO` и валидирован
- CHECK DEFAULT логически исключает весь новый диапазон и находится в состоянии ENFORCED
- структура, NOT NULL, CHECK и индексы заготовки сверены с родителем
- нет мешающих долгих транзакций или autovacuum для предотвращения wraparound
- `lock_timeout` установлен в той же транзакции, где выполняется ATTACH
- после успеха проверены дерево секций, DEFAULT и актуальность статистики родителя
Частые вопросы
ATTACH PARTITION блокирует всю секционированную таблицу?
На родителе ATTACH использует SHARE UPDATE EXCLUSIVE, совместимую с обычным чтением и записью. ACCESS EXCLUSIVE берётся на присоединяемую таблицу и DEFAULT. Поэтому чаще блокируется не весь кластер и не все секции, а запросы, которым нужен DEFAULT. В разобранном случае почти вся текущая запись шла именно туда, поскольку новая месячная секция ещё не была присоединена.
Почему PostgreSQL сканирует пустой DEFAULT-раздел?
Серверу требуется доказательство, что в DEFAULT нет строк нового диапазона. Если подходящего валидного и принудительно применяемого CHECK нет, PostgreSQL проверяет содержимое сам. У логически пустой, но раздутой таблицы скан может прочитать множество физических страниц. Исключение из документации — DEFAULT, реализованный как foreign table: для него такая проверка пропускается.
Достаточно ли добавить CHECK как NOT VALID?
Нет. `NOT VALID` пропускает первоначальный скан при добавлении и начинает проверять новые изменения, но не подтверждает старые строки. Затем нужно выполнить `VALIDATE CONSTRAINT`; только после этого `convalidated` станет true. В PostgreSQL 18 ограничение также должно быть ENFORCED, то есть иметь `conenforced = true`.
Границы CHECK должны совпадать с FOR VALUES посимвольно?
Нет, PostgreSQL проверяет логическое следствие, а не равенство строк SQL. Однако простые сравнения над одной колонкой и одинаково типизированными константами надёжнее. Для RANGE нижняя граница включительна, верхняя исключительна. При `timestamptz` лучше явно указывать тип и смещение времени.
Стоит ли оставлять DEFAULT-раздел?
Он полезен как ловушка для данных вне подготовленных диапазонов, но требует строгого мониторинга, участвует в каждом ATTACH и запрещает `DETACH PARTITION CONCURRENTLY`. Если приложение умеет повторять отклонённые вставки и старые секции регулярно отцепляются, отсутствие DEFAULT часто проще. Если потеря входного события недопустима, DEFAULT можно оставить, но его надо контролировать и разбирать до ротации.
Что делать, если ATTACH уже ждёт, а запись остановилась?
Найдите PID команды и её блокирующую цепочку через `pg_stat_activity` и `pg_locks`. Если безопаснее вернуть запись, отмените ожидающий ATTACH функцией `pg_cancel_backend()` и повторите его после устранения причины. Не завершайте вслепую autovacuum для предотвращения wraparound. На будущие запуски задайте короткий `lock_timeout` в транзакции ATTACH.
Гарантирует ли CHECK мгновенный ATTACH?
Нет. Он устраняет проверочный скан, но ACCESS EXCLUSIVE на новой и DEFAULT-таблицах всё равно запрашивается. Кроме того, ATTACH может создавать отсутствующие индексы. Быстрое завершение получается только при сочетании валидных ограничений, заранее подготовленных индексов, отсутствия долгих держателей блокировок и короткого `lock_timeout`.
Источники
- PostgreSQL 18 — Table Partitioning — Раздел 5.12.2.2 Partition Maintenance: ATTACH через отдельную таблицу, CHECK для пропуска сканов новой и DEFAULT-секций, рекурсивная проверка подсекций. https://www.postgresql.org/docs/18/ddl-partitioning.html
- PostgreSQL 18 — ALTER TABLE — Синтаксис и семантика ATTACH/DETACH PARTITION, режимы блокировок, VALIDATE CONSTRAINT, LIST и NULL, запрет DETACH PARTITION CONCURRENTLY при наличии DEFAULT. https://www.postgresql.org/docs/18/sql-altertable.html
- PostgreSQL 18 — CREATE TABLE — Описание `LIKE`, `INCLUDING ALL`, копирования CHECK, generated- и identity-колонок, индексов и параметров хранения; блокировка формы PARTITION OF. https://www.postgresql.org/docs/18/sql-createtable.html
- PostgreSQL 18.0 Release Notes — Релиз от 25 сентября 2025 года: `ANALYZE ONLY`, хранение NOT NULL в `pg_constraint`, поддержка NOT VALID для именованных NOT NULL и поле `conenforced`. https://www.postgresql.org/docs/release/18.0/
- PostgreSQL 18 — Routine Vacuuming — Раздел 24.1: autovacuum не обрабатывает саму секционированную таблицу; обычный autovacuum прерывается конфликтующей блокировкой, кроме запуска для предотвращения wraparound. https://www.postgresql.org/docs/18/routine-vacuuming.html
- PostgreSQL 18 — ANALYZE — Официальный синтаксис `ANALYZE ONLY table_name`, поведение для секционированных таблиц и требования к блокировкам. https://www.postgresql.org/docs/18/sql-analyze.html
- pg_partman 5.5.0 Documentation — Документация зафиксированного релиза: `premake`, `check_default()`, retention и процедуры переноса данных из DEFAULT. https://github.com/pgpartman/pg_partman/blob/v5.5.0/doc/pg_partman.md
- pg_partman 5.5.0 Release Notes — Официальные примечания к актуальному на дату проверки релизу 5.5.0: изменения роли background worker и исправления уязвимостей повышения привилегий. https://github.com/pgpartman/pg_partman/releases/tag/v5.5.0
