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

Проблемы в проде случаются не потому, что PostgreSQL "капризный", а потому что миграция почти никогда не про одну схему. Она затрагивает сразу четыре вещи: структуру таблиц, реальные данные, поведение приложения и порядок релиза. Если выпадает хотя бы одно звено, миграция может формально пройти "успешно" и при этом оставить систему в странном состоянии.
Типичный сценарий: вы добавили колонку или поменяли тип, база обновилась, а код еще не готов (или уже раскатан и ожидает другое). В результате получаете ошибки в рантайме, неожиданный план запроса и нагрузку, или блокировки, которые проявляются только на боевом объеме.
Отдельная зона риска - необратимые изменения. Любой DROP, перезапись значений, UPDATE без четких условий, пересоздание индексов и таблиц - это потенциально билет в один конец. Еще хуже "тихая" порча данных: миграция заполняет значения по умолчанию, обрезает строки, конвертирует типы с потерей точности или склеивает значения. Замечают это часто через неделю. Ошибок не было, но качество данных стало другим.
Есть несколько симптомов, что миграция спроектирована плохо:
"Без сюрпризов" означает управляемость. Вы заранее знаете, что будет блокироваться и на сколько, можете проверяемо сравнить результат (до и после), а откат реалистичен: не только технически возможен, но и понятен по шагам и последствиям. Поэтому от генератора миграций стоит требовать не только SQL, но и сценарий выполнения: порядок релиза, проверки данных и безопасный план отката, если что-то пошло не так.
Если написать просто "сделай миграцию", модель почти всегда начнет угадывать. Она может добавить лишние индексы, переписать ограничения или предложить опасный ALTER TABLE, который зависнет на проде. Лучше дать точные входные данные и попросить конкретные артефакты.
Минимальный набор контекста обычно такой: текущая схема таблиц (DDL или хотя бы список колонок с типами), связанные индексы и внешние ключи, пример 5-10 строк (обезличенных), примерные объемы (сколько строк и как быстро растет таблица), а также что именно считается "успехом" после изменения.
Полезно заранее задать формат ответа, чтобы ничего не потерялось. Например: отдельные блоки "SQL (up)", "SQL (down)", "Проверки", "Риски", "План релиза", "Откат". Тогда результат легче читать, ревьюить и запускать по шагам.
Еще одно правило, которое стоит прямо написать в запросе: одна миграция - одна цель. Если цель добавить колонку, пусть миграция добавляет колонку и только. Без "заодно переименуем", без массовых оптимизаций и без изменений, которые не относятся к задаче.
Короткий список того, что стоит требовать в ответе:
Пример запроса: "Добавляем поле phone_normalized в users, таблица 20 млн строк, есть индекс по email, записи обновляются постоянно. Нужна миграция без длительных блокировок, с проверками заполнения и откатом, если после релиза рост ошибок > 1%".
Качество миграции почти всегда упирается не в SQL, а в контекст. Если не дать деталей о схеме и данных, модель будет угадывать, а угадывания в проде стоят дорого.
Начните с короткого "паспорта" текущей базы. Достаточно текста, без портянок на тысячи строк:
Дальше опишите нагрузку и то, что должно оставаться быстрым. Не "у нас много трафика", а конкретно: какие запросы критичны, какие страницы или фоновые джобы их вызывают, и какие поля участвуют в фильтрах и сортировках. Пример: "Список заказов: 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) и переключаете приложение. И только после этого ужесточаете ограничения и убираете старое.
Практично сразу просить разнести работу на несколько миграций, а не "одну большую":
Так снижается риск, и откат становится реальным.
Иногда честный 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, который ловит ситуацию "все прошло, но стало неверно": совпадение количества строк, проверка инвариантов, контрольные выборки на типичных кейсах.
Удобный формат требований:
Отдельно стоит просить "предохранители" не только в комментариях, а прямо как часть плана выполнения:
В конце попросите формат фиксации результатов для команды релиза: какие цифры записать в лог и что именно вставить в релиз-заметку (null_cnt, orphan_cnt, количество измененных строк).
Хорошая миграция в PostgreSQL - это не один SQL-файл, а короткий план релиза с точками возврата. Когда просите сгенерировать миграции, просите сразу и сценарий выката: что делаем первым, что вторым, и на каком шаге еще можно безопасно откатиться.
Самая надежная стратегия - expand-contract: сначала расширяем схему так, чтобы старый и новый код работали одновременно, потом переключаем трафик на новый код, и только после этого удаляем старое.
Обычно хватает 2-3 этапов:
Ключевая просьба к модели: для каждого этапа указать, до какого состояния можно откатиться без потерь. После этапа 1 откат почти всегда прост (потому что мы только добавляли). После этапа 2 все зависит от того, начали ли вы писать данные в новую структуру так, что старый код их уже не понимает.
Просите не общий текст, а конкретику: команды, проверки и критерии остановки.
Для долгих операций отдельно нужен сценарий: как делать 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 NULLsource не заполнен или не входит в списокОткат здесь редко означает "вернуть все как было". Реалистичный откат: снять NOT NULL (и CHECK, если добавляли), остановить backfill, и временно вернуть старый путь в коде, который не требует source. Новую логику включайте только когда NULL ровно ноль, проверки чистые, и запись новых строк уже всегда проставляет source.
Сюрпризы начинаются с формулировки задачи. Когда вы пишете "сделай миграцию", модель вынуждена додумывать: какие таблицы, сколько данных, какой трафик, и есть ли у релиза окно на простой.
Ошибки, которые чаще всего приводят к проблемам в проде:
Вместо "одного SQL-файла" просите результат как план: отдельные шаги для схемы, для данных и для очистки, с ожидаемым влиянием на блокировки. Добавьте порядок деплоя (сначала код или сначала миграция), и попросите "красные флажки": если backfill затрагивает больше N строк или длится дольше X минут - остановиться и перейти к откату.
Перед тем как нажать "выполнить", проверьте не только SQL, но и весь сценарий. Даже если миграцию помог сгенерировать ИИ, ответственность все равно на вас.
Сначала убедитесь, что у изменения есть обратная дорога. Идеально, когда есть up и down. Если down невозможен (например, вы удаляете данные или меняете тип с потерей точности), нужен компенсирующий план: что именно вы делаете, чтобы быстро вернуть сервис в рабочее состояние.
Дальше проверьте измеримость успеха. "Миграция прошла" ничего не значит без проверок. Нужны запросы до и после и заранее заданные пороги: сколько строк должно обновиться, сколько NULL допустимо, какие значения считаются ошибкой.
Затем убедитесь, что изменение разбито на безопасные шаги (обычно expand-contract): сначала добавляем новое, затем начинаем писать в оба места или пишем совместимо, потом читаем из нового, и только после этого убираем старое.
Короткий список перед запуском:
Мини-пример: добавляете поле email_confirmed_at. В безопасном варианте вы сначала добавляете nullable-колонку, выкатываете код, который пишет в нее при подтверждении, затем бэкфиллом заполняете старым пользователям, проверяете долю NULL, и только потом добавляете ограничения (если они вообще нужны). Если что-то идет не так, вы откатываете код и просто перестаете использовать колонку.
Чтобы миграции перестали быть лотереей, важнее всего не "сильный промпт один раз", а повторяемый ритуал. Команде нужен один стандарт запроса и правило: мы не меняем структуру запроса каждый раз по настроению. Тогда сравнивать результаты проще, а качество растет от релиза к релизу.
Зафиксируйте, какие артефакты вы обязаны получать на выходе. Не "описание словами", а пакет, который можно проверить, запустить и откатить:
Закрепите правило прогонов: любую миграцию сначала гоняйте на копии данных, максимально близкой к продакшену по объему и распределению значений. Результаты проверок фиксируйте: сколько строк изменилось, сколько NULL, сколько нарушений. Если цифры "плавают", значит у вас не описаны входные условия или допущения.
Если вы ведете проект в TakProsto (takprosto.ai), добавьте в этот ритуал два простых пункта: сначала прогон в planning mode с явным планом шагов и проверок, затем точка возврата перед продом (снапшот и понятный rollback-порядок). Это не отменяет SQL-откат, но помогает быстрее вернуться в рабочее состояние, если что-то пошло не так.
После релиза не закрывайте задачу сразу. Сделайте короткий постмортем на 10 минут: что было непонятно в запросе, какие проверки не сработали, где план отката оказался дырявым. Запишите одно улучшение в стандарт промпта и одно улучшение в шаблон миграции. Процесс станет сильнее без героизма и ночных созвонов.
Лучший способ понять возможности ТакПросто — попробовать самому.