Когда схема базы данных в порядке, а данные — нет
Вы только что выполнили миграцию базы данных, которая добавила новую колонку. Всё выглядело хорошо. Изменение схемы прошло успешно, колонка существует, приложение работает. Но тут кто-то замечает: все пользователи, зарегистрировавшиеся три года назад, должны были быть помечены как верифицированные, но они не помечены. Или номера телефонов, которые вы перенесли, теперь имеют несовместимые форматы, потому что старые данные не соответствовали новым правилам.
Схема корректна. Тип колонки правильный. Ограничения валидны. Проблема в самих данных.
Это ситуация, которая ощущается хуже, чем неудачная миграция. Неудачная миграция очевидна. Вы видите ошибку, знаете, что что-то сломалось, и можете действовать. Но миграция, которая проходит успешно, но с плохими данными, — это скрытая проблема. Она может находиться в продакшене часами или днями, прежде чем кто-то заметит. И когда вы замечаете, естественный порыв — паниковать и искать способы всё отменить.
Но откат схемы для исправления данных создает новый набор проблем. Приложение может перестать работать со старой схемой. Вы можете потерять данные, которые уже были корректны. И вы отменяете структурное изменение, которое на самом деле было правильным, только чтобы исправить содержимое, которое было неверным.
Настоящая проблема — не в схеме
Когда миграция добавляет колонку, например is_verified, со значением по умолчанию false, изменение схемы тривиально. Колонка существует, значение по умолчанию работает, и новые записи будут вести себя корректно. Проблема в том, что существующие пользователи, которые должны быть верифицированы, теперь помечены как неверифицированные. Схема этого не вызвала. Логика миграции этого не вызвала. Пробел был в понимании того, какими должны были быть существующие данные.
Другой распространенный пример: миграция изменяет хранение номеров телефонов, требуя коды стран. Новый формат корректен, и новые записи будут следовать правилам. Но старые номера телефонов, которые были сохранены без кодов стран, теперь несовместимы. Схема в порядке. Данные — нет.
В обоих случаях решение не в откате схемы. Решение — исправить данные, сохранив схему нетронутой.
Компенсирующие скрипты: исправление данных без изменения структуры
Компенсирующий скрипт — это миграция, которая изменяет только данные, а не схему. Он выполняется как обычная миграция, проходит через тот же пайплайн и следует тому же процессу развертывания. Но вместо ALTER TABLE, CREATE INDEX или ADD COLUMN он содержит только операторы UPDATE, INSERT или DELETE, которые исправляют данные.
Цель проста: привести данные в корректное состояние без изменения структуры таблицы.
Вот практический пример. После добавления колонки currency со значением по умолчанию 'IDR' команда понимает, что все транзакции от международных партнеров должны использовать 'USD'. Компенсирующий скрипт выглядит так:
UPDATE transactions SET currency = 'USD' WHERE partner_type = 'international';
Никаких изменений схемы. Никаких новых колонок. Никаких преобразований типов. Просто целевая коррекция данных.
Компенсирующие скрипты также обрабатывают частичные сбои миграции. Представьте миграцию, которая создает новую таблицу и переносит данные из старой. Некоторые строки не удается перенести из-за нарушения ограничения. Вместо отката всей миграции и начала заново, компенсирующий скрипт может обработать оставшиеся строки. Он вставляет или обновляет только те записи, которые были пропущены, без повторного выполнения полной миграции.
Делайте ваши компенсирующие скрипты идемпотентными
Есть одно правило, которое важнее любого другого: компенсирующие скрипты должны быть идемпотентными. Выполнение скрипта дважды должно давать тот же результат, что и однократное выполнение.
Это не теоретическое соображение. На практике миграции могут выполняться повторно. Пайплайн перезапускается. Окружение обновляется. Кто-то запускает миграцию вручную во время отладки. Если ваш скрипт не идемпотентен, его повторное выполнение может повредить данные.
Решение простое. Всегда проверяйте текущее состояние перед внесением изменений. Используйте предложение WHERE, которое достаточно конкретно, чтобы затронуть только строки, требующие коррекции. Если ваша база данных поддерживает это, используйте предложения ON CONFLICT для вставок.
Вместо этого:
UPDATE transactions SET currency = 'USD' WHERE partner_type = 'international';
Напишите это:
UPDATE transactions SET currency = 'USD' WHERE partner_type = 'international' AND currency IS DISTINCT FROM 'USD';
Разница небольшая, но критическая. Вторая версия обновляет только те строки, где валюта еще не равна 'USD'. Выполнение ее сто раз затронет только те строки, которые нужно изменить, и только при первом запуске.
Когда компенсирующих скриптов недостаточно
Компенсирующие скрипты — не универсальное решение. Они работают, когда схема корректна и нужно исправить только данные. Если сама схема неверна, вам все равно потребуется правильная миграция схемы.
Например, если вы добавили колонку с неправильным типом данных или если ограничения колонки слишком строги для данных, которые необходимо хранить, компенсирующий скрипт не поможет. Вам нужно изменить схему. Аналогично, если миграция внесла ошибку, которая повредила данные таким образом, что их нельзя исправить простыми операторами UPDATE, может потребоваться более сложный подход.
Но для распространенного случая, когда схема верна, а данные неверны, компенсирующие скрипты безопаснее любой альтернативы. Они позволяют избежать рисков обратных миграций, не требуют восстановления из резервных копий и могут выполняться, пока приложение продолжает обслуживать трафик.
Краткий чек-лист для написания компенсирующих скриптов
- Убедитесь, что схема корректна, прежде чем писать скрипт. Если структура нуждается в изменении, сначала займитесь этим.
- Оформляйте скрипт как новую миграцию, а не как горячее исправление, применяемое напрямую к базе данных.
- Делайте каждый оператор идемпотентным. Проверяйте условия перед обновлением или вставкой.
- Тестируйте скрипт на копии продакшен-данных, а не только на пустой базе данных.
- Включайте логирование или комментарии, объясняющие, почему требуется коррекция данных, чтобы будущие члены команды понимали контекст.
Вывод
Когда миграция идет не так, естественный порыв — всё отменить. Но откат схемы — это тяжелая операция, которая может сломать приложение и потерять корректные данные. Компенсирующие скрипты дают вам более легкий и точный инструмент. Они позволяют исправить данные, сохранив работающую схему. В следующий раз, когда вы увидите миграцию, которая прошла успешно, но оставила после себя плохие данные, спросите себя: схема неверна или данные неверны? Если ответ — данные, напишите компенсирующий скрипт. Это быстрее, безопаснее и менее разрушительно, чем откат.