Как с помощью Claude Code готовить безопасные миграции PostgreSQL: pattern expand/contract, бэкафиллы, планы отката и что проверять в staging перед релизом.

Изменение схемы PostgreSQL выглядит простым, пока не встретится с реальным трафиком и реальными данными. Риск обычно не в самом SQL. Риск — когда код приложения, состояние базы и тайминг деплоя перестают совпадать.
Большинство сбоев практические и болезненные: деплой ломает систему, потому что старый код обращается к новому столбцу; миграция блокирует горячую таблицу и растёт число таймаутов; или «быстрое» изменение тихо удаляет или переписывает данные. Даже если ничего не падает, вы можете выпустить тонкие баги: неверные значения по умолчанию, сломанные ограничения или индексы, которые так и не успели построиться.
AI‑сгенерированные миграции добавляют ещё один слой риска. Инструменты могут создать валидный SQL, который всё равно небезопасен для вашей нагрузки, объёма данных или процесса релиза. Они могут угадывать имена таблиц, не заметить долгие блокировки или смазать план отката, потому что down‑миграции — это сложно. Если вы используете Claude Code для миграций, нужны страховки и конкретный контекст.
Когда в этой статье говорится, что изменение «безопасно», это значит три вещи:
Цель — чтобы миграции стали рутинной работой: предсказуемыми, тестируемыми и скучными.
Начните с нескольких неумолимых правил. Они держат модель в рамках и не позволяют отгружать изменение, которое работает только на вашем ноутбуке.
Разбивайте работу на маленькие шаги. Изменение схемы, бэкафилл, изменение приложения и шаг очистки — это разные риски. Совмещать их сложнее — хуже видно, что сломалось, и сложнее откатиться.
Отдавайте предпочтение добавлению перед разрушением. Добавление столбца, индекса или таблицы обычно низко рисково. Переименование или удаление объектов — там и случаются простои. Сначала сделайте безопасную часть, переключите приложение, и удаляйте старое только когда уверены, что оно не используется.
Сделайте приложение терпимым к обеим формам данных на некоторое время. Код должен уметь читать либо старый, либо новый столбец во время rollout. Это избегает гонки, когда некоторые сервера уже работают на новом коде, а база ещё старая (или наоборот).
Относитесь к миграциям как к коду для продакшна, а не как к быстрому скрипту. Даже если вы собираете приложение на платформе вроде Koder.ai (Go backend с PostgreSQL и React или Flutter клиентами), база данных — это общий ресурс. Ошибки стоят дорого.
Если нужен компактный набор правил для вставки в начало каждой подсказки для генерации SQL, используйте примерно такое:
Практический пример: вместо переименования столбца, от которого зависит приложение, добавьте новый столбец, постепенно заполните его, задеплойте код, который читает новый, потом старый, и лишь потом удаляйте старый столбец.
Claude может писать приличный SQL по расплывчатому запросу, но безопасные миграции требуют контекст. Относитесь к подсказке как к мини‑техническому заданию: покажите, что есть, объясните, что нельзя ломать, и определите, что значит «безопасно» для вашего rollout.
Начните с вставки только тех фактов о базе, которые действительно важны. Включите определение таблицы и релевантные индексы и ограничения (PK, UNIQUE, FK, CHECK, триггеры). Если вовлечены связанные таблицы — вставьте их фрагменты тоже. Маленький, точный отрывок не даст модели угадывать имена или пропускать важное ограничение.
Добавьте реальные масштабы. Число строк, объём таблицы, скорость записей и пиковый трафик меняют план. «200M строк и 1k записей/с» — это совсем другая миграция, чем «20k строк и в основном чтения». Также укажите версию Postgres и как у вас выполняются миграции (в одной транзакции или несколькими шагами).
Опишите, как приложение использует данные: важные чтения, записи и фоновые задания. Примеры: «API читает по email», «воркеры обновляют статус» или «отчёты сканируют по created_at». Это определяет, нужен ли expand/contract, флаги фич и насколько безопасен бэкафилл.
Наконец, будьте явны насчёт ограничений и ожидаемых результатов. Простая структура работает хорошо:
Запрос и SQL + план заставляют модель думать о порядке, рисках и проверках перед релизом.
Паттерн expand/contract меняет базу данных PostgreSQL без разрыва приложения во время изменений. Вместо одного рискованного переключения вы даёте базе возможность поддерживать старую и новую форму данных в течение некоторого времени.
Думайте так: сначала добавьте новое безопасно (expand), постепенно переключайте трафик и данные, и только потом уберите старые элементы (contract). Это особенно полезно при работе с AI, потому что заставляет планировать «грязную» середину.
Практический поток выглядит так:
NOT VALID, когда это уместно).Используйте этот паттерн всякий раз, когда пользователи могут оставаться на старой версии приложения во время изменения базы. Это включает развёртывания с множеством инстансов, мобильные приложения с медленным обновлением или любой релиз, где миграция может идти минуты или часы.
Полезный приём — планировать два релиза. Релиз 1 делает expand и compatibility так, чтобы ничего не ломалось при неполном бэкафилле. Релиз 2 делает contract только после подтверждения, что новый код и новые данные на месте.
Скопируйте этот шаблон и заполните скобки. Он заставляет Claude Code подготовить SQL, который можно запустить, проверки и реалистичный план отката.
You are helping me plan a PostgreSQL expand-contract migration.
Context
- App: [what the feature does, who uses it]
- Database: PostgreSQL [version if known]
- Table sizes: [rough row counts], write rate: [low/medium/high]
- Zero/near-zero downtime required: [yes/no]
Goal
- Change: [describe the schema change]
- Current schema (relevant parts):
[paste CREATE TABLE or \\d output]
- How the app will change (expand phase and contract phase):
- Expand: [new columns/indexes/triggers, dual-write, read preference]
- Contract: [when/how we stop writing old fields and remove them]
Hard safety requirements
- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.
- If any step can block writes, call it out explicitly and suggest alternatives.
- Use small, reversible steps. No “big bang” changes.
Deliverables
1) UP migration SQL (expand)
- Use clear comments.
- If you propose indexes, tell me if they should be created CONCURRENTLY.
- If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.
2) Verification queries
- Queries to confirm the new schema exists.
- Queries to confirm data is being written to both old and new structures (if dual-write).
- Queries to estimate whether the change caused bloat/slow queries/locks.
3) Rollback plan (realistic)
- DOWN migration SQL (only if it is truly safe).
- If down is not safe, write a rollback runbook:
- how to stop the app change
- how to switch reads back
- what data might be lost or need re-backfill
4) Runbook notes
- Exact order of operations (including app deploy steps).
- What to monitor during the run (errors, latency, deadlocks, lock waits).
- “Stop/continue” checkpoints.
Output format
- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md
Две дополнительные строки, полезные на практике:
RISK: blocks writes, и указать, когда его запускать (off‑peak vs anytime).Маленькие изменения схем всё равно могут навредить, если они берут долгие блокировки, переписывают большие таблицы или падают на полпути. Когда вы используете Claude Code для миграций, просите SQL, который избегает переписей и сохраняет работоспособность приложения, пока база догоняет.
Добавление nullable столбца обычно безопасно. Добавление столбца с NOT NULL и дефолтом может быть рискованным на старых версиях Postgres, потому что это может переписать всю таблицу.
Безопасный подход — двухшаговое изменение: добавьте столбец как NULL без дефолта, заполните батчами, затем поставьте дефолт для новых строк и сделайте NOT NULL после проверки.
Если нужно немедленно обеспечить дефолт, требуйте объяснения про поведение блокировок для вашей версии Postgres и запасной план, если время выполнения окажется больше ожидаемого.
Для индексов на больших таблицах просите CREATE INDEX CONCURRENTLY, чтобы чтения и записи продолжали работать. Также отметьте, что это нельзя выполнять внутри транзакционного блока, а значит ваше средство миграции должно уметь запускать не‑транзакционные шаги.
Для внешних ключей безопаснее сначала добавить их как NOT VALID, а потом валидировать. Это делает начальную операцию быстрее, а FK всё же защищает новые записи.
При ужесточении ограничений (NOT NULL, UNIQUE, CHECK) просите «сначала почистить, потом включить». Миграция должна найти плохие строки, исправить их, а затем уже включать ограничение.
Удаляйте объекты только после полного цикла релиза и подтверждения, что никто их не читает.
Если нужен короткий чеклист для подсказок:
Бэкафиллы — это место, где чаще всего проявляется боль от миграций, а не сам ALTER TABLE. Самые безопасные подсказки рассматривают бэкафиллы как контролируемые задания: измеримые, перезапускаемые и щадящие для продакшна.
Начните с приёмных проверок, которые легко запустить и тяжело оспорить: ожидаемые числы строк, целевой уровень NULL и ряд spot‑проверок (например, сравнить старые и новые значения для 20 случайных ID).
Попросите план батчирования. Батчи держат блокировки короткими и уменьшают сюрпризы. Хороший запрос указывает:
Требуйте идемпотентности, потому что бэкафилл может упасть на полпути. SQL должен быть безопасен для повторного запуска без дублирования или порчи данных. Типичные паттерны: "обновлять только там, где new_col IS NULL" или детерминированное правило, где один и тот же ввод всегда даёт одинаковый вывод.
Также опишите, как приложение остаётся корректным во время бэкафилла. Если новые записи продолжают приходить, нужен мост: dual‑write в коде, временный триггер или read‑fallback (читать новое, если есть, иначе старое). Скажите, какой подход вы сможете безопасно задеплоить.
Наконец, заложите паузу и возобновление в дизайн. Попросите отслеживание прогресса и чекпоинты, например небольшую таблицу с последним обработанным ID и запрос, который показывает прогресс (обновлённые строки, последний ID, время начала).
Пример: вы добавляете users.full_name, вычисляемый из first_name и last_name. Безопасный бэкафилл обновляет только строки, где full_name IS NULL, идёт по ID‑диапазонам, фиксирует последний обработанный ID и гарантирует корректность новых регистраций через dual‑write до полного переключения.
План отката — это не просто «напишите down‑миграцию». Это две задачи: отмена изменения схемы и работа с данными, которые могли измениться, пока новая версия была жива. Откат схемы часто возможен. Откат данных часто — нет, если не предусмотреть его заранее.
Будьте явны, что означает откат для вашего изменения. Если вы удаляете столбец или переписываете значения на месте, требуйте честного ответа: "Откат восстановит совместимость приложения, но исходные данные нельзя вернуть без снапшота." Такая честность спасает.
Попросите чёткие триггеры отката, чтобы в инциденте не было спора. Примеры:
Требуйте полный пакет отката, а не только SQL: down‑миграция (только если безопасна), переключение в конфиге/коде приложения для совместимости и как остановить фоновые задания.
Обычно полезна такая подсказка:
Produce a rollback plan for this migration.
Include: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.
State what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.
Include rollback triggers with thresholds.
Перед релизом сделайте «safety snapshot», чтобы сравнить до и после:
Также будьте ясны, когда не стоит откатываться. Если вы просто добавили nullable столбец и приложение dual‑write, исправление вперёд (hotfix, пауза бэкафилла, затем возобновление) часто безопаснее, чем откат и ещё большая рассинхронизация.
AI может быстро писать SQL, но он не видит вашу продовую базу. Большинство провалов происходит, когда подсказка расплывчата и модель «додаёт» недостающие детали.
Распространённая ловушка — пропуск текущей схемы. Если вы не вставили определение таблицы, индексы и ограничения, SQL может ссылаться на несуществующие столбцы или не учесть уникальность, и бэкафилл превратится в медленную операцию с блокировками.
Ещё одна ошибка — отправить expand, backfill и contract в одном релизе. Это убирает точку возврата. Если бэкафилл идёт долго или падает, вы останетесь с приложением, которое ожидает финального состояния.
Чаще всего встречаются:
Конкретный пример: "переименовать столбец и обновить приложение." Если план переименования и бэкафилла делает всё в одной транзакции, медленный бэкафилл удержит блокировки и сломает живой трафик. Безопасная подсказка вынуждает мелкие батчи, явные таймауты и проверки перед удалением старого пути.
Staging выявляет проблемы, которые не проявляются в маленькой dev‑базе: долгие блокировки, неожиданные NULL, отсутствующие индексы и забытые пути в коде.
Сначала проверьте, что схема соответствует плану после миграции: столбцы, типы, дефолты, ограничения и индексы. Один пропущенный индекс может превратить безопасный бэкафилл в катастрофу.
Запустите миграцию на реалистичных данных. Идеально — недавняя копия продакшена с маской чувствительных полей. Если так нельзя, хотя бы смоделируйте объёмы и «горячие точки» (большие таблицы, широкие строки, многоиндексные таблицы). Запишите тайминги по каждому шагу, чтобы понимать ожидания в проде.
Короткий чеклист для staging:
Наконец, тестируйте реальные пользовательские потоки, а не только SQL. Создавайте, обновляйте и читайте записи, затронутые изменением. Если план expand/contract, подтвердите, что обе схемы работают до финального cleanup.
Предположим, у вас есть users.name, где хранят полные имена вроде "Ada Lovelace". Вы хотите first_name и last_name, но нельзя сломать регистрацию, профили или админку во время rollout.
Начните с expand‑шага, который безопасен даже если код пока не менялся: добавьте nullable столбцы, оставьте старый столбец и избегайте долгих блокировок.
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
Затем обновите поведение приложения для поддержки обеих форм. В Релизе 1 приложение должно читать из новых колонок, если они есть, иначе падать обратно на name, и писать в оба столбца, чтобы новые данные были согласованы.
Дальше идёт бэкафилл. Запустите батчевую задачу, которая обновляет небольшие куски строк за раз, фиксирует прогресс и может быть безопасно приостановлена. Например: обновлять users, где first_name IS NULL, по возрастающему ID, по 1,000 строк за раз и логировать число изменённых строк.
Перед ужесточением правил проверьте в staging:
first_name и last_name и всё ещё пишут namenameusers не стали заметно медленнееРелиз 2 переключает чтение только на новые столбцы. Лишь после этого добавляйте ограничения (например, SET NOT NULL) и удаляйте name в отдельном последующем деплое.
Для отката — держите всё просто. Приложение продолжает читать name в процессе перехода, а бэкафилл можно остановить. Если нужно откатить Релиз 2, переключите чтения обратно на name и оставьте новые столбцы до стабилизации.
Относитесь к каждому изменению как к маленькому runbook. Цель не в идеальной подсказке, а в рутине, которая требует нужные детали: схема, ограничения, план запуска и откат.
Стандартизируйте, что должна содержать каждая заявка на миграцию:
Решите заранее, кто за что отвечает. Простое разделение ролей предотвращает ситуацию «каждый думал, что кто‑то другой это сделал»: разработчики готовят подсказку и миграционный код, ops отвечает за тайминг и мониторинг в проде, QA проверяет staging и крайние случаи, и один человек принимает финальное go/no‑go.
Если вы строите приложения через чат, полезно заранее описать последовательность, прежде чем генерировать SQL. Для команд, использующих Koder.ai, Planning Mode — естественное место для записи этой последовательности; снапшоты и откаты помогут сократить радиус поражения при неожиданностях.
После релиза запланируйте очистку (contract) сразу, пока контекст свеж — чтобы старые столбцы и временный совместимый код не оставались месяцами.
Изменение схемы рискует, когда код приложения, состояние базы и тайминг релиза перестают совпадать.
Типичные сценарии отказа:
Используйте подход expand/contract:
Так обе версии приложения остаются работоспособными во время отката.
Модель может сгенерировать корректный SQL, который тем не менее небезопасен для вашего масштаба или процесса релиза.
Типичные риски, связанные с AI:
Рассматривайте AI‑выход как черновик и требуйте плана выполнения, проверок и шагов отката.
Вставьте в подсказку только факты, от которых зависит миграция:
CREATE TABLE фрагменты (индексы, FK, UNIQUE/CHECK, триггеры)Это предотвращает угадывание и заставляет модель думать о порядке операций.
Правило по умолчанию: разделяйте их.
Практический разрыв:
Сворачивание всего в один шаг лишает вас точки возврата и усложняет диагностику.
Предпочтительный паттерн:
ADD COLUMN ... NULL без дефолта (быстро)NOT NULL только после верификацииДобавление ненулевого дефолта сразу может перезаписать всю таблицу на старых версиях Postgres.
Просите CREATE INDEX CONCURRENTLY для больших/горячих таблиц и отметьте, что он не запускается внутри транзакции (ваше средство миграции должно это поддерживать).
Укажите ожидаемое время выполнения и метрики для мониторинга (ожидание блокировок, латентность запросов). Для проверки — сравните EXPLAIN план до/после в staging.
Добавляйте FK как NOT VALID сначала, затем валидируйте:
NOT VALID делает шаг менее затратнымVALIDATE CONSTRAINT отдельно, когда сможете наблюдать за процессомТак ограничения применяются к новым записям, а тяжёлая валидация выполняется по расписанию.
Хороший бэкафилл — это батчированный, идемпотентный и перезапускаемый.
Практические требования:
WHERE new_col IS NULL)Это делает бэкафилл выживаемым при реальной нагрузке.
Цель отката: быстро восстановить совместимость приложения, даже если данные не удастся полностью вернуть.
Рабочий план отката должен включать:
Часто самый безопасный откат — вернуть чтение на старое поле, оставив новые столбцы на месте.