Во время поэтапного релиза нужно переименовать поле в большой таблице без остановки сервиса. Как провести изменение схемы при одновременно работающих старой и новой версиях приложения?
Используйте стратегию expand-and-contract: сначала расширьте схему, затем временно обеспечьте совместимость старой и новой версий приложения, перенесите данные, переключите чтение и только после завершения перехода удалите старое поле. Нельзя сначала переименовывать или удалять используемую колонку: старая версия приложения немедленно начнёт получать ошибки.
Изменение схемы раньше часто выполняли как единую операцию во время остановки приложения. Для небольших таблиц это приемлемо, но на больших объёмах блокировка, перестроение индексов или длительная миграция делают простой простой недопустимым.
Стратегия expand-and-contract появилась как практический способ поддерживать rolling deployment, когда разные версии приложения некоторое время работают одновременно. Она разделяет несовместимое изменение на несколько совместимых этапов.
Старый код может читать и записывать старое поле, а новый — новое. Если колонку сразу переименовать, удалить или сделать обязательной без подготовки данных, одна из версий начнёт завершаться ошибкой.
Даже добавление новой колонки не решает проблему полностью: старый код не будет заполнять её, а новый код может прочитать пустое значение. Дополнительные риски — неполный backfill, расхождение значений при параллельной записи, блокировки таблицы и невозможность безопасного отката.
Типичная последовательность выглядит так:
NULL.Минимальная часть миграции схемы может выглядеть так:
Сам SQL не обеспечивает синхронизацию во время перехода. Пока работают обе версии приложения, запись в оба поля должна выполняться в одном месте: например, в прикладном слое или через временный триггер. Прикладная синхронизация обычно прозрачнее для системы, а триггер может упростить защиту от записей, которые выполняются не через приложение, но усложняет отладку и миграцию.
Backfill нельзя безоговорочно выполнять одним большим запросом. Он может занять длительную блокировку, создать всплеск нагрузки на журнал транзакций и конкурировать с рабочими запросами. Безопаснее использовать порции, ограничивать скорость, отслеживать ошибки и повторять обработку идемпотентно.
Перед удалением старого поля нужно проверить не только код приложения, но и отчёты, фоновые задания, ETL-процессы, индексы, ограничения и внешние интеграции. Если откат релиза должен быть возможен, новая схема обязана временно поддерживать и старую версию приложения.
Допустим, в таблице пользователей поле name нужно разделить на first_name и last_name. Вариант с немедленным удалением name прост, но несовместим со старой версией приложения и требует остановки или синхронного обновления всех потребителей.
Вариант с временным представлением, скрывающим сложность схемы, может уменьшить изменения в приложении, но создаёт дополнительный слой совместимости и не решает проблему записи в несколько атрибутов. Вариант с триггером автоматически поддерживает новые поля, однако бизнес-логика миграции становится неявной.
Практичный выбор — добавить новые поля, выпустить совместимый код с записью старого и новых представлений, выполнить контролируемый backfill, проверить расхождения и затем переключить чтение. Это требует нескольких релизов и временного удвоения записи, зато позволяет сохранить доступность, выполнить откат и контролировать нагрузку.
Как обеспечить корректность данных, если запись произошла во время backfill?
Backfill и текущая запись конкурируют за одни и те же строки. Если сначала скопировать старое значение, а затем сразу изменить его только в старом поле, новое поле станет устаревшим.
Поэтому до начала backfill нужно включить механизм двойной записи или синхронизации изменений. Дополнительно применяют повторный проход, сравнение значений или обработку изменений по журналу событий. Одного начального копирования недостаточно.
Почему новую колонку не всегда следует сразу сделать обязательной?
Старые строки ещё могут не содержать значения, а старая версия приложения не умеет его записывать. Немедленное ограничение NOT NULL может сделать backfill невозможным или привести к ошибкам текущих операций.
Обычно сначала добавляют колонку с допускаемым отсутствием значения, заполняют существующие записи и переводят все записи на новый путь записи. Ограничение добавляют отдельным этапом после проверки полноты данных. При этом нужно учитывать способ добавления ограничения конкретной СУБД: оно может потребовать проверки всей таблицы или блокировки.
Когда стратегия expand-and-contract недостаточна?
Она не устраняет физическую стоимость тяжёлой миграции. Если изменение требует длительной перестройки таблицы, большого дополнительного объёма диска или несовместимо с возможностями конкретной СУБД, даже поэтапный процесс может создать неприемлемую нагрузку.
В таком случае рассматривают специализированные инструменты онлайн-миграции, создание новой таблицы с постепенной репликацией данных, временное разделение нагрузки или изменение модели хранения. Выбор зависит от размера данных, допустимого окна повышенной нагрузки, требований к откату и гарантий согласованности.