Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Expand-contract: безопасное изменение схемы

Expand-contract: изменение схемы базы без простоя и с возможностью отката

Expand-contract: три фазы изменения схемы, сосуществование старой и новой структуры, двойная запись, переключение чтения и удаление устаревшего.

Прямой ответ

Изменение схемы одним шагом требует остановки: старая версия кода не работает с новой структурой, а новая — со старой. Подход expand-contract разбивает изменение на три фазы, в каждой из которых работают обе версии кода.

Фаза 1 — Expand. Новая структура добавляется рядом со старой, старая остаётся без изменений:

ALTER TABLE users ADD COLUMN full_name text;
-- старая колонка name остаётся

Код прежней версии продолжает работать, поскольку не знает о новой колонке.

Фаза 2 — Migrate. Код пишет в обе структуры, существующие данные переносятся:

def create_user(first: str, last: str) -> None:
    db.execute(
        "INSERT INTO users (name, full_name) VALUES (%s, %s)",
        (f"{first} {last}", f"{first} {last}"),
    )

Чтение остаётся из старой структуры до завершения переноса и проверки; о механизме — в статье про двойную запись.

Фаза 3 — Contract. Чтение переводится на новую структуру, старая удаляется:

ALTER TABLE users DROP COLUMN name;

Ключевое свойство подхода — в любой момент работает и прежняя, и новая версия кода. Развёртывание не требует согласованного переключения, а откат сводится к возврату прежней версии приложения.

Порядок фаз и точки отката:

Фаза Состояние схемы Откат
Expand Старая + новая Удаление новой колонки
Migrate Старая + новая, данные в обеих Прекращение записи в новую
Contract Только новая Восстановление из копии

После удаления старой структуры откат требует восстановления данных, поэтому между фазами выдерживают интервал; о переносе данных — в статье про миграцию данных.

Распространённая ошибка — сокращение подхода до двух фаз: добавление новой структуры и удаление старой в одном релизе. В этом окне прежняя версия кода обращается к удалённой колонке и падает.

Изменения, которые не сводятся к expand-contract

Подход покрывает большинство изменений структуры, но три случая требуют иного плана.

Изменение типа данных с преобразованием значений. Перевод строки в число либо даты в отметку времени требует преобразования каждой строки, и часть значений может не преобразоваться. План включает обработку исключений, а не только добавление колонки.

Разделение таблицы. Одна таблица заменяется двумя, и запись во время перехода должна попадать в обе структуры. Фаза двойной записи усложняется, поскольку преобразование затрагивает связи.

Изменение первичного ключа. Ключ используется во внешних связях, поэтому изменение затрагивает все зависимые таблицы. Перенос выполняется по всем связям одновременно, и порядок имеет значение.

В этих случаях expand-contract остаётся основой — старая структура сохраняется до подтверждения, — но фаза переноса требует отдельного плана с обработкой исключений и проверкой связей.

Отдельный случай — изменение, не требующее переноса данных. Добавление необязательной колонки без значения по умолчанию выполняется одной командой и не затрагивает существующие строки. Применение подхода к такому изменению создаёт лишние фазы без пользы.

Критерий применимости: если существующие строки требуют изменения, нужен expand-contract с переносом. Если изменение затрагивает только структуру и новые записи, достаточно одной миграции схемы.

Практика: проведение изменения по фазам

1. Разделяйте фазы отдельными релизами. Каждая фаза разворачивается и проверяется отдельно, что ограничивает область проблемы.

2. Добавляйте новую структуру без ограничений. Ограничение NOT NULL на новой колонке до заполнения данных нарушит запись прежней версии кода.

3. Включайте двойную запись до переноса данных. Иначе изменения, внесённые во время переноса, потеряются.

4. Проверяйте согласованность структур. Сверка старой и новой структуры до переключения чтения выявляет ошибки преобразования.

5. Выдерживайте интервал перед удалением. Неделя-две между переключением чтения и удалением старой структуры сохраняют возможность отката.

6. Описывайте миграции в инструменте. Инструмент миграций фиксирует порядок применения и позволяет откатить изменение схемы; о настройке — в статье про Alembic.

7. Учитывайте индексы. Индекс по новой колонке создаётся параллельно, чтобы не блокировать запись во время создания.

8. Проверяйте каждую фазу отдельно. Тесты прежней версии кода должны проходить после каждой фазы, включая последнюю.

Рекомендации:

  1. Не удаляйте старую структуру в том же релизе. Окно одновременной работы версий требует сосуществования структур.
  2. Избегайте переименования колонок прямой командой. Переименование — это удаление и добавление, поэтому выполняется через новую колонку.
  3. Учитывайте длительные транзакции. Изменение схемы может ожидать завершения долгих транзакций и блокировать очередь запросов.
  4. Фиксируйте фазу в документации. Разработчик должен знать, в какой фазе находится изменение и что можно удалять.
  5. Проверяйте откат на стенде. Возврат к прежней версии после каждой фазы должен работать.

Практический сценарий: падение сервиса при одношаговом переименовании колонки

Контекст: требовалось переименовать колонку для соответствия соглашению об именовании. Изменение выполнили одной командой вместе с релизом.

  1. Диагностика. После применения миграции и развёртывания новой версии часть запросов падала с ошибкой о несуществующей колонке. Старые поды продолжали работать и обращались к прежнему имени.
  2. Причина. Переименование выполнялось как атомарная операция, но развёртывание происходило постепенно. В окне между применением миграции и завершением обновления старая версия кода обращалась к удалённому имени.
  3. Действия. (а) Миграцию откатили, вернув прежнее имя; (б) изменение перевели на три фазы: добавление новой колонки, двойная запись и перенос данных, переключение чтения и удаление старой; (в) между фазами выдержали интервал в неделю для проверки; (г) добавили правило: изменение схемы, затрагивающее имена колонок, выполняется только через expand-contract; (д) добавили проверку, что прежняя версия кода проходит тесты после каждой фазы.
  4. Результат. Переименование выполнено за три релиза без простоя, а откат оставался возможным до последней фазы.

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

FAQ

Зачем три фазы, если можно переименовать колонку? Переименование ломает прежнюю версию кода. При постепенном развёртывании обе версии работают одновременно, поэтому структура должна поддерживать обе.

Сколько держать старую структуру? До подтверждения корректности новой. Обычно одну-две недели после переключения чтения.

Что делать с ограничением NOT NULL на новой колонке? Добавлять после заполнения данных. Ограничение на незаполненной колонке нарушит запись прежней версии кода.

Можно ли сократить до двух фаз? Только если развёртывание выполняется одномоментно с остановкой. При постепенном развёртывании окно несовместимости неизбежно.

Источники: Martin Fowler (Parallel Change), PostgreSQL docs (ALTER TABLE), Alembic docs — ссылки в шапке.