Claude Code для миграций PostgreSQL: без сюрпризов в проде
Claude Code для миграций PostgreSQL: как формулировать запросы для обратимых изменений, проверок данных и плана отката, чтобы не терять продакшен-данные.

Почему миграции в PostgreSQL дают сюрпризы
Проблемы в проде случаются не потому, что PostgreSQL "капризный", а потому что миграция почти никогда не про одну схему. Она затрагивает сразу четыре вещи: структуру таблиц, реальные данные, поведение приложения и порядок релиза. Если выпадает хотя бы одно звено, миграция может формально пройти "успешно" и при этом оставить систему в странном состоянии.
Типичный сценарий: вы добавили колонку или поменяли тип, база обновилась, а код еще не готов (или уже раскатан и ожидает другое). В результате получаете ошибки в рантайме, неожиданный план запроса и нагрузку, или блокировки, которые проявляются только на боевом объеме.
Отдельная зона риска - необратимые изменения. Любой DROP, перезапись значений, UPDATE без четких условий, пересоздание индексов и таблиц - это потенциально билет в один конец. Еще хуже "тихая" порча данных: миграция заполняет значения по умолчанию, обрезает строки, конвертирует типы с потерей точности или склеивает значения. Замечают это часто через неделю. Ошибок не было, но качество данных стало другим.
Есть несколько симптомов, что миграция спроектирована плохо:
- нет понятного отката или откат "на словах" (например, "вернем бэкап")
- время выполнения непредсказуемо: на стейдже было 2 секунды, в проде стало 20 минут
- миграция смешивает DDL и тяжелые DML-операции без ограничений по батчам
- нет проверок: не измеряется, сколько строк затронуто и что именно поменялось
- релиз требует "идеального" порядка действий и ручных шагов, которые легко перепутать
"Без сюрпризов" означает управляемость. Вы заранее знаете, что будет блокироваться и на сколько, можете проверяемо сравнить результат (до и после), а откат реалистичен: не только технически возможен, но и понятен по шагам и последствиям. Поэтому от генератора миграций стоит требовать не только SQL, но и сценарий выполнения: порядок релиза, проверки данных и безопасный план отката, если что-то пошло не так.
Что именно просить у Claude Code, чтобы получить полезный результат
Если написать просто "сделай миграцию", модель почти всегда начнет угадывать. Она может добавить лишние индексы, переписать ограничения или предложить опасный ALTER TABLE, который зависнет на проде. Лучше дать точные входные данные и попросить конкретные артефакты.
Минимальный набор контекста обычно такой: текущая схема таблиц (DDL или хотя бы список колонок с типами), связанные индексы и внешние ключи, пример 5-10 строк (обезличенных), примерные объемы (сколько строк и как быстро растет таблица), а также что именно считается "успехом" после изменения.
Полезно заранее задать формат ответа, чтобы ничего не потерялось. Например: отдельные блоки "SQL (up)", "SQL (down)", "Проверки", "Риски", "План релиза", "Откат". Тогда результат легче читать, ревьюить и запускать по шагам.
Еще одно правило, которое стоит прямо написать в запросе: одна миграция - одна цель. Если цель добавить колонку, пусть миграция добавляет колонку и только. Без "заодно переименуем", без массовых оптимизаций и без изменений, которые не относятся к задаче.
Короткий список того, что стоит требовать в ответе:
- SQL up и SQL down (или честное объяснение, почему down ограничен)
- проверки до и после (SELECT, счетчики, поиск NULL/дубликатов)
- оценка блокировок и рисков по времени
- план релиза по шагам (что делать первым, что вторым)
- план отката (что откатываем, что оставляем как есть)
Пример запроса: "Добавляем поле phone_normalized в users, таблица 20 млн строк, есть индекс по email, записи обновляются постоянно. Нужна миграция без длительных блокировок, с проверками заполнения и откатом, если после релиза рост ошибок > 1%".
Подготовка контекста перед генерацией миграции
Качество миграции почти всегда упирается не в SQL, а в контекст. Если не дать деталей о схеме и данных, модель будет угадывать, а угадывания в проде стоят дорого.
Начните с короткого "паспорта" текущей базы. Достаточно текста, без портянок на тысячи строк:
- таблицы, которых касается изменение, ключевые поля (PK), внешние ключи (FK) и что они защищают
- ограничения: UNIQUE, NOT NULL, CHECK, дефолты и триггеры, если есть
- индексы: какие уже есть, какие реально используются, какие нельзя ломать
Дальше опишите нагрузку и то, что должно оставаться быстрым. Не "у нас много трафика", а конкретно: какие запросы критичны, какие страницы или фоновые джобы их вызывают, и какие поля участвуют в фильтрах и сортировках. Пример: "Список заказов: WHERE user_id = ? ORDER BY created_at DESC LIMIT 50. Должно отвечать < 200 мс".
Отдельно зафиксируйте требования к простоям и блокировкам. Укажите окно релиза и допустимы ли длинные блокировки таблиц. Если простой недопустим, так и пишите: "Нужен безопасный план без длительных ACCESS EXCLUSIVE locks, предпочтительно в несколько шагов".
Наконец, сформулируйте, что считается валидными данными, и где есть исключения. Это помогает предложить проверки до миграции и корректную обработку "грязных" записей. Примеры: "email обычно уникален, но в старых данных встречаются дубликаты" или "status может быть NULL у импортированных строк".
Пошаговый шаблон запроса: миграция, проверки и откат
Хороший результат начинается не с SQL, а с четкого запроса. Самый надежный способ снизить риск - заставить модель сначала зафиксировать цель, ограничения и критерии успеха, а уже потом писать миграции.
Сформулируйте цель одним предложением, как для тикета: что меняется и зачем. Затем добавьте ограничения: нужен ли режим без даунтайма, должна ли миграция быть обратимой, сколько версий приложения будут жить параллельно, и какие операции запрещены (например, переписывание всей таблицы в пике нагрузки).
Просите не просто up/down SQL, а up/down с предпосылками и комментариями: какие блокировки возможны, какие индексы создавать CONCURRENTLY, что нельзя выполнять в транзакции, что безопасно сделать "онлайн".
Шаблон запроса
Скопируйте и заполните скобки. Важно: не просите "сделай миграцию" - попросите полный план.
Роль: ты опытный DBA/PostgreSQL инженер.
Контекст:
- PostgreSQL: (версия)
- Таблица(ы): (DDL или описание колонок, PK/FK, индексы)
- Нагрузка: (примерно RPS, размер таблицы, критичные запросы)
- Приложение: (как используется поле, сколько версий может быть одновременно)
Цель (1 предложение): (например: добавить users.phone для верификации)
Ограничения:
- Без даунтайма: (да/нет)
- Обратимость: down должен возвращать схему в исходное состояние (да/нет)
- Совместимость: старая версия кода должна работать после up (да/нет)
Сгенерируй:
1) Up SQL (с комментариями, предпосылками и предупреждениями о блокировках)
2) Down SQL (что теряется, что нельзя вернуть, как минимизировать риск)
3) Проверки ДО: запросы, которые должны пройти, и пороги (0 ошибок, 0 NULL и т.п.)
4) Проверки ПОСЛЕ: запросы и критерии успешности
5) Риски: топ-3 риска и как их снизить
6) План поэтапного релиза: порядок действий (миграция, выкладка кода, бэкфилл, переключение, уборка)
7) План отката: условия отката, шаги, и как убедиться, что откат сработал
Если изменение потенциально необратимо (например, удаление колонки), просите предложить безопасную альтернативу через expand-contract: сначала расширение схемы, потом перенос данных, и только затем удаление старого.
Как добиться обратимости и предсказуемости изменений
Предсказуемая миграция - это не "изменить схему", а провести изменение так, чтобы его можно было безопасно раскатить, проверить и при необходимости откатить без потери данных.
Самые спокойные изменения обычно не ломают существующие записи: добавить nullable-колонку, создать новый индекс (желательно CONCURRENTLY), завести новую таблицу рядом со старой. Сюрпризы чаще всего прилетают при переименованиях, смене типа, добавлении NOT NULL и UNIQUE без подготовки или при удалении колонок.
Рабочий прием - expand-contract. Сначала расширяете схему так, чтобы старый и новый код могли жить вместе. Потом заполняете данные (backfill) и переключаете приложение. И только после этого ужесточаете ограничения и убираете старое.
Практично сразу просить разнести работу на несколько миграций, а не "одну большую":
- миграция 1: только изменение схемы
- миграция 2: backfill батчами с прогрессом
- миграция 3: ограничения и уборка
Так снижается риск, и откат становится реальным.
Иногда честный down SQL невозможен (например, вы удалили данные или сузили тип). В этом случае полезнее компенсирующий откат: как временно вернуть совместимость приложения, как остановить запись в новую схему, какие точки возврата использовать (снапшот/бэкап, отметка версии, заморозка релиза). Это лучше, чем делать вид, что "down есть".
Проверки данных: что обязательно спросить у ИИ
Главная причина сюрпризов в миграциях - не DDL, а данные. Поэтому просите не только SQL для изменения схемы, но и SQL для проверок до и после. Так вы заранее увидите, где миграция сломается или "молча" изменит смысл данных.
Перед миграцией попросите короткий набор запросов, которые ищут проблемные данные и показывают масштаб. Хорошо, если вместе с запросами будут пороги: что считаем допустимым, а что стоп-сигналом.
Пример формулировки: "Дай pre-check SQL: дубликаты по ключу X, NULL в будущих NOT NULL полях, строки-сироты для FK, и укажи, при каких значениях миграцию нельзя запускать".
-- pre-check: дубликаты
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- pre-check: будущий NOT NULL
SELECT COUNT(*) AS null_cnt
FROM orders
WHERE status IS NULL;
-- pre-check: нарушения будущего FK
SELECT COUNT(*) AS orphan_cnt
FROM orders o
LEFT JOIN users u ON u.id = o.user_id
WHERE o.user_id IS NOT NULL AND u.id IS NULL;
После миграции нужен post-check, который ловит ситуацию "все прошло, но стало неверно": совпадение количества строк, проверка инвариантов, контрольные выборки на типичных кейсах.
Удобный формат требований:
- сравнить количество строк до и после для таблиц A/B (допустимое отклонение 0)
- проверить инварианты: нет NULL там, где теперь NOT NULL; нет дублей по уникальному индексу; FK не нарушены
- дать 2-3 контрольные выборки для ручной проверки бизнес-смысла
Отдельно стоит просить "предохранители" не только в комментариях, а прямо как часть плана выполнения:
- где можно использовать транзакцию, а где нельзя (и почему)
- таймауты (lock_timeout, statement_timeout) перед опасными шагами
- порядок действий, чтобы не повесить прод
- условия остановки (например, через DO-блок), если пороги превышены
В конце попросите формат фиксации результатов для команды релиза: какие цифры записать в лог и что именно вставить в релиз-заметку (null_cnt, orphan_cnt, количество измененных строк).
План релиза и отката: чтобы миграция не привязала вам руки
Хорошая миграция в PostgreSQL - это не один SQL-файл, а короткий план релиза с точками возврата. Когда просите сгенерировать миграции, просите сразу и сценарий выката: что делаем первым, что вторым, и на каком шаге еще можно безопасно откатиться.
Expand-contract: как не ломать старый код
Самая надежная стратегия - expand-contract: сначала расширяем схему так, чтобы старый и новый код работали одновременно, потом переключаем трафик на новый код, и только после этого удаляем старое.
Обычно хватает 2-3 этапов:
- этап 1 (expand): добавить новые поля или таблицы, индексы CONCURRENTLY, триггеры или дефолты, но ничего не удалять
- этап 2 (code): выкатить код, который пишет в новое и читает совместимо
- этап 3 (contract): удалить старые поля, ограничения или триггеры, когда данные заполнены и код переключен
Ключевая просьба к модели: для каждого этапа указать, до какого состояния можно откатиться без потерь. После этапа 1 откат почти всегда прост (потому что мы только добавляли). После этапа 2 все зависит от того, начали ли вы писать данные в новую структуру так, что старый код их уже не понимает.
Что включить в план отката
Просите не общий текст, а конкретику: команды, проверки и критерии остановки.
- команды отката: точные SQL для отмены изменений этапа, либо миграция down, либо отдельный rollback-скрипт
- проверки: что сравниваем до и после (количество строк, доля NULL, расхождения по ключам, наличие блокировок)
- критерии стопа: порог ошибок, время блокировки, рост лагов реплики, неожиданный рост размера таблицы
- точки принятия решения: после какого шага откат уже рискованный и нужен отдельный план восстановления
Для долгих операций отдельно нужен сценарий: как делать backfill порциями (батчи по id или по диапазонам), как ставить на паузу и продолжать, и как хранить прогресс (таблица прогресса или список диапазонов).
Пример сценария: добавляем поле без потери продакшен-данных
Представьте таблицу orders, и вам нужно добавить обязательное поле source (например: web, mobile, partner). В проде уже миллионы строк, и если сразу сделать NOT NULL, миграция упадет или надолго заблокирует запись.
Почти всегда работает один и тот же путь: сначала расширяем схему, затем заполняем данные, и только потом ужесточаем ограничения.
Как сформулировать запрос
Чтобы получить результат без неприятных сюрпризов, просите не только SQL, но и порядок действий, проверки и реалистичный откат. Например, так:
Сгенерируй миграцию для PostgreSQL для таблицы orders:
- добавить колонку source TEXT NULL
- backfill: заполнить source для старых строк порциями по 10_000, чтобы не держать долгие блокировки
- после backfill: поставить NOT NULL и (если нужно) CHECK на допустимые значения
Дай:
1) SQL для up/down
2) отдельные SQL-проверки: сколько строк осталось с source IS NULL, примеры проблемных строк
3) план выката: на каком шаге можно остановиться, и что делать в коде
4) критерии готовности для включения новой логики в приложении
Критерии готовности и откат
Попросите проверки, которые дают однозначный ответ, можно ли идти дальше. Обычно достаточно:
- счетчик
NULLпосле backfill и доNOT NULL - выборка 20-50 строк, где
sourceне заполнен или не входит в список - оценка времени backfill по батчам и безопасный лимит на одну транзакцию
Откат здесь редко означает "вернуть все как было". Реалистичный откат: снять NOT NULL (и CHECK, если добавляли), остановить backfill, и временно вернуть старый путь в коде, который не требует source. Новую логику включайте только когда NULL ровно ноль, проверки чистые, и запись новых строк уже всегда проставляет source.
Частые ошибки в запросах к Claude Code
Сюрпризы начинаются с формулировки задачи. Когда вы пишете "сделай миграцию", модель вынуждена додумывать: какие таблицы, сколько данных, какой трафик, и есть ли у релиза окно на простой.
Ошибки, которые чаще всего приводят к проблемам в проде:
- мало контекста: нет DDL, размеров таблиц, индексов, внешних ключей, и модель "угадывает" связи и типы
- нет требований к обратимости и проверкам: вы получаете движение только вперед
- один большой шаг "схема + данные": добавление колонки, заполнение, смена NOT NULL и удаление старого поля в одном скрипте превращаются в долгую транзакцию
- игнорирование совместимости с кодом: порядок деплоя другой, и приложение начинает писать в несуществующую колонку или ломается чтение
- нет стоп-критериев: когда релиз нужно остановить и откатываться
Вместо "одного SQL-файла" просите результат как план: отдельные шаги для схемы, для данных и для очистки, с ожидаемым влиянием на блокировки. Добавьте порядок деплоя (сначала код или сначала миграция), и попросите "красные флажки": если backfill затрагивает больше N строк или длится дольше X минут - остановиться и перейти к откату.
Короткий чеклист перед запуском миграции в прод
Перед тем как нажать "выполнить", проверьте не только SQL, но и весь сценарий. Даже если миграцию помог сгенерировать ИИ, ответственность все равно на вас.
Сначала убедитесь, что у изменения есть обратная дорога. Идеально, когда есть up и down. Если down невозможен (например, вы удаляете данные или меняете тип с потерей точности), нужен компенсирующий план: что именно вы делаете, чтобы быстро вернуть сервис в рабочее состояние.
Дальше проверьте измеримость успеха. "Миграция прошла" ничего не значит без проверок. Нужны запросы до и после и заранее заданные пороги: сколько строк должно обновиться, сколько NULL допустимо, какие значения считаются ошибкой.
Затем убедитесь, что изменение разбито на безопасные шаги (обычно expand-contract): сначала добавляем новое, затем начинаем писать в оба места или пишем совместимо, потом читаем из нового, и только после этого убираем старое.
Короткий список перед запуском:
- есть путь назад: down-миграция или компенсирующий откат с командами и условиями
- есть проверки до и после: конкретные SQL-запросы и критерии "норма/ошибка"
- изменение разбито на этапы, и ясно, где можно остановиться без поломки
- понятны риски по нагрузке: где возможны блокировки, сколько это может длиться, нужны ли таймауты и окно низкой активности
- есть план релиза по шагам: порядок выката приложения и миграции, и точка остановки, где сервис остается рабочим
Мини-пример: добавляете поле email_confirmed_at. В безопасном варианте вы сначала добавляете nullable-колонку, выкатываете код, который пишет в нее при подтверждении, затем бэкфиллом заполняете старым пользователям, проверяете долю NULL, и только потом добавляете ограничения (если они вообще нужны). Если что-то идет не так, вы откатываете код и просто перестаете использовать колонку.
Следующие шаги: как превратить это в стабильный процесс
Чтобы миграции перестали быть лотереей, важнее всего не "сильный промпт один раз", а повторяемый ритуал. Команде нужен один стандарт запроса и правило: мы не меняем структуру запроса каждый раз по настроению. Тогда сравнивать результаты проще, а качество растет от релиза к релизу.
Зафиксируйте, какие артефакты вы обязаны получать на выходе. Не "описание словами", а пакет, который можно проверить, запустить и откатить:
- SQL для изменений схемы (в том числе обратная миграция, если она возможна)
- скрипты проверок данных до и после (с ожидаемыми цифрами или порогами)
- список рисков и ограничений (блокировки, время выполнения, влияние на индексы)
- порядок релиза по шагам (когда выкатывать код, когда миграцию, когда backfill)
- план отката: условия, команды и что будет с данными при откате
Закрепите правило прогонов: любую миграцию сначала гоняйте на копии данных, максимально близкой к продакшену по объему и распределению значений. Результаты проверок фиксируйте: сколько строк изменилось, сколько NULL, сколько нарушений. Если цифры "плавают", значит у вас не описаны входные условия или допущения.
Если вы ведете проект в TakProsto (takprosto.ai), добавьте в этот ритуал два простых пункта: сначала прогон в planning mode с явным планом шагов и проверок, затем точка возврата перед продом (снапшот и понятный rollback-порядок). Это не отменяет SQL-откат, но помогает быстрее вернуться в рабочее состояние, если что-то пошло не так.
После релиза не закрывайте задачу сразу. Сделайте короткий постмортем на 10 минут: что было непонятно в запросе, какие проверки не сработали, где план отката оказался дырявым. Запишите одно улучшение в стандарт промпта и одно улучшение в шаблон миграции. Процесс станет сильнее без героизма и ночных созвонов.