Когда удаление столбца в базе данных ломает продакшен: управление деструктивными изменениями схемы
У вас есть миграция базы данных, которая удаляет неиспользуемый столбец. SQL выглядит чистым. Миграция выполняется без ошибок. Но через пять минут начинают срабатывать оповещения. Продакшен-приложение выбрасывает ошибки, потому что часть кода всё ещё ссылается на этот столбец. То, что выглядело как простая чистка, привело к простою.
Этот сценарий встречается чаще, чем большинство команд готовы признать. Проблема не в самой миграции. Проблема в предположении, что удаление чего-либо из базы данных безопасно только потому, что вы думаете, что это больше никто не использует.
Что делает изменение деструктивным
Изменения базы данных делятся на две категории. Аддитивные изменения добавляют что-то новое: новый столбец, новую таблицу, новый индекс. Они обычно безопасны, потому что существующий код просто игнорирует то, о чём не знает.
Деструктивные изменения удаляют, переименовывают или изменяют существующие структуры. Удаление столбца, переименование таблицы, изменение типа столбца или удаление ограничения — всё это деструктивно. Риск очевиден: если какое-либо работающее приложение всё ещё зависит от этой структуры, оно сломается в момент применения изменения.
Опасность усиливается современными стратегиями развёртывания. Rolling updates и blue-green развёртывания означают, что старая и новая версии приложения работают бок о бок в течение минут или часов. Деструктивная миграция, выполняемая во время развёртывания, немедленно сломает старые экземпляры, которые всё ещё обслуживают трафик.
Паттерн многофазной миграции
Самый безопасный подход — никогда не удалять ничего за один шаг. Вместо этого разбейте деструктивные изменения на несколько фаз. Каждая фаза должна быть совместима с версией приложения, работающей в этот момент.
Следующая диаграмма иллюстрирует три фазы безопасного переименования столбца, показывая, какие версии приложения совместимы на каждом этапе.
Рассмотрим переименование столбца из status в status_code. Одиночная миграция, которая переименовывает столбец, сломает любой код, всё ещё читающий status. Многофазный подход выглядит так:
Фаза 1: Добавьте новый столбец, не удаляя старый. Скопируйте данные из старого столбца в новый. Обновите код приложения, чтобы он читал из нового столбца, но продолжал писать в оба. Разверните это изменение.
Фаза 2: После подтверждения, что все экземпляры приложения используют новый столбец, прекратите запись в старый столбец. Обновите код, чтобы он ссылался только на status_code. Разверните снова.
Фаза 3: Когда вы уверены, что ни один работающий код не касается старого столбца, удалите его отдельной миграцией. Запланируйте это на часы низкой нагрузки.
Тот же паттерн применим к удалению таблиц. Создайте представление или новую таблицу, заменяющую старую функциональность. Перенаправьте код приложения на новую структуру. Подождите, пока ни один код не будет ссылаться на старую таблицу. Затем удалите её.
Мягкое удаление как страховочная сеть
Иногда вы хотите удалить данные из представления приложения, не удаляя их из базы данных. Здесь помогает мягкое удаление.
Вместо выполнения DELETE добавьте столбец, например deleted_at или is_active. Приложение фильтрует удалённые строки с помощью WHERE. Данные остаются в таблице для аудита, восстановления или неожиданных зависимостей от других функций.
Мягкое удаление особенно полезно, когда вы не полностью уверены, нужны ли данные. Это даёт вам буфер безопасности. Если что-то сломается, вы можете восстановить видимость без восстановления базы данных. Компромисс в том, что таблицы становятся больше, а запросы должны учитывать фильтр. Но для многих команд этот компромисс стоит безопасности.
Осторожное обращение с ограничениями
Удаление ограничения, такого как внешний ключ или уникальное ограничение, менее рискованно, чем удаление данных, но всё равно имеет последствия. Ограничения обеспечивают целостность данных. Если ваш код приложения полагается на базу данных для предотвращения дублирования записей или потерянных записей, удаление ограничения может привести к повреждению данных.
Перед удалением ограничения проверьте кодовую базу, чтобы убедиться, что никакая логика от него не зависит. Если ваша база данных это поддерживает, рассмотрите возможность отключения ограничения вместо его удаления. Это позволит вам протестировать влияние без потери возможности снова его включить.
Практический чеклист для деструктивных изменений
- Убедитесь, что ни один работающий код приложения не ссылается на структуру, которую вы планируете удалить. Проверьте как текущий релиз, так и любые выполняющиеся развёртывания.
- Разделите изменение как минимум на две миграции: одну для добавления новой структуры и перенаправления кода, другую для удаления старой структуры после периода ожидания.
- Выполняйте деструктивные миграции отдельно от развёртывания функций. Не объединяйте удаление столбца с релизом новой конечной точки.
- Планируйте деструктивные изменения на окна низкой нагрузки. Даже при многофазном планировании с неожиданными проблемами легче справиться, когда затронуто меньше пользователей.
- После удаления старых структур выполните очистку остатков, таких как переименованные столбцы или отключённые ограничения, в последующей миграции. Но помните: очистка также деструктивна, поэтому применяйте тот же многофазный подход.
Основной принцип
Никогда не удаляйте то, к чему всё ещё может обращаться работающее приложение. Это звучит очевидно, но это самая распространённая ошибка, которую команды допускают при миграциях баз данных. Давление поддерживать схему в чистоте, предположение, что «это больше никто не использует», и желание выполнить одну чистую миграцию вместо нескольких маленьких — всё это подталкивает команды к рискованным одношаговым удалениям.
Многофазные миграции требуют больше времени и больше развёртываний. Но они предотвращают те простои продакшена, которые превращают простую чистку схемы в экстренный откат. Чистая схема не стоит сломанного приложения.