← Все статьи

SiteGuard12 мин чтения

Как изменить структуру базы данных без остановки сайта: expand-and-contract

Миграция базы данных без простоя начинается с совместимости приложения и данных. Пока сайт принимает заказы, старые процессы могут читать прежнее поле, новые — заполнять другое, а фоновая задача — переносить историю. Если эти действия не согласованы, успешное выполнение SQL-команды ещё не означает безопасное изменение системы.

Подход expand-and-contract разделяет изменение на этапы: добавить совместимую структуру, подготовить все пути записи, перенести данные, проверить новое чтение и только потом удалить прежнее. Он помогает избежать запланированного отключения сайта, но не гарантирует отсутствие блокировок и ошибок. Разберём, как организовать переход и определить границу, после которой обычный возврат кода уже не подходит.

Начните с договора между кодом и схемой

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

Руководство Prisma по expand and contract описывает постепенное появление новой структуры рядом со старой и переход клиентов к ней. Практическая задача команды — определить допустимые сочетания схемы и приложений на каждом этапе. Последовательность фиксируют до релиза, включая возможность остановиться между шагами.

Для владельца сайта полезен простой вопрос подрядчику: что произойдёт, если во время миграции один старый обработчик завершит заказ? Если ответ неизвестен, совместимость кода и схемы базы ещё не доказана. Тестировать нужно создание, изменение и чтение реальных по структуре объектов, включая ошибки и повторные попытки.

Учебный пример и список всех писателей

Представим условный сайт, где способ доставки хранится в поле delivery_method текстом: courier или pickup. Команда хочет перейти к delivery_method_id: значениям 10 и 20 из справочника. На время перехода соответствие зафиксировано и обратимо; новые способы доставки не вводятся. Это упрощённый пример, а не готовая миграция для любого магазина.

Версия A знает только текстовое поле. Подготовительная версия B читает его же, но при изменениях сохраняет оба представления. Версия C читает идентификатор и продолжает поддерживать оба поля. Завершающая версия D работает только с новым полем. Такая последовательность позволяет отдельно проверить перенос данных, переключение чтения и удаление зависимости от старой структуры.

Сначала найдите все источники записи: веб-приложение, мобильный API, административную панель, воркеры, импорт заказов, интеграции и служебные скрипты. Уточните, какие процессы долго живут после развёртывания и какие задания используют старый код. Обновлённый сайт в браузере не доказывает, что ночной импорт уже перестал записывать прежний формат.

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

Добавьте совместимую структуру

На этапе расширения новое поле появляется рядом со старым. В нашем примере оно сначала допускает отсутствие значения, чтобы версия A продолжала создавать записи. Старый столбец и его смысл сохраняются. Ограничения новой модели вводят в таком порядке, который не запрещает допустимые операции работающих версий.

Не заполняйте неизвестные значения удобной константой ради прохождения ограничения. Если в исторических данных встретилось partner_delivery, а утверждённое соответствие содержит только два варианта, запись требует отдельного решения. Назначение идентификатора 10 по умолчанию изменило бы способ доставки и создало бизнес-ошибку, хотя технически поле выглядело бы заполненным.

До применения проверьте зависимости, объём таблицы, доступное место и поведение точной команды на используемой версии СУБД. Изменение, быстрое на пустой тестовой базе, может иначе выполняться на большой таблице с активными транзакциями. Проверка старой версии приложения после расширения обязательна: этап считается совместимым по результату, а не по названию миграции.

Онлайн DDL всё равно может ждать и блокировать

Отсутствие переписывания всей таблицы не означает отсутствия блокировки. Документация PostgreSQL для ALTER TABLE указывает, что требуемый уровень блокировки зависит от операции, а по умолчанию используется ACCESS EXCLUSIVE, если не оговорено иное. Поэтому конкретную команду оценивают отдельно, включая время ожидания её выполнения.

В документации MySQL 8.4 об ограничениях online DDL также описаны эксклюзивные блокировки метаданных и ожидание транзакций. Надпись «online» не является обещанием, что операция никак не повлияет на запросы сайта. Возможность выбранного алгоритма зависит от изменения, таблицы и версии сервера.

В плане задайте допустимое ожидание, условия отмены запуска и человека, который оценит блокирующую транзакцию. Не следует автоматически завершать чужие запросы ради продвижения миграции. Сначала выясняют их назначение и последствия прерывания. Настройки ожидания применяют адресно, понимая поведение СУБД при отказе или отмене команды.

Во время изменения наблюдают за задержкой пользовательских операций, ожиданиями блокировок, ошибками и состоянием реплик. Если операция уже выполняется, остановка тоже может требовать времени и ресурсов. Возможность приостановить собственный backfill нельзя переносить на любую DDL-команду: это разные механизмы.

Подготовьте согласованную запись в оба представления

Версия B должна вычислять текст и идентификатор из одного подтверждённого изменения способа доставки. В рамках одной базы предпочтительно сохранять связанную пару в общей транзакции. Два независимых запроса, между которыми процесс может завершиться, оставляют риск частичного результата. Если данные находятся в разных системах, требуется отдельный механизм согласования; слово dual-write не делает их атомарными.

Пока одновременно работают A и B, новым значениям ещё нельзя доверять как полному актуальному набору. Версия A способна изменить текст после того, как B заполнила идентификатор. Тогда оба поля существуют, но описывают разные способы доставки. Простая проверка «новое поле не пустое» такой дефект не обнаружит.

В нашем плане доверие к новому представлению начинается только после подтверждённого завершения всех писателей A и их незавершённых транзакций. Затем выполняется полная сверка и backfill. Другой проект может использовать проверенный механизм синхронизации на уровне базы, но его правила и влияние на нагрузку также нужно испытать. Два одновременно действующих механизма синхронизации без ясного владельца истины добавляют неоднозначность.

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

Защитите backfill от конкурентных изменений

Backfill — фоновое заполнение новой структуры по существующим данным. Он работает одновременно с клиентами, поэтому не должен записывать устаревшее значение поверх свежего. В учебной гонке обработчик читает для заказа 730 способ courier, пользователь меняет его на pickup, а backfill позже пытается записать идентификатор 10. Без защиты получается неверная пара.

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

UPDATE orders
SET delivery_method_id = :mapped_id,
    row_revision = row_revision + 1
WHERE id = :order_id
  AND row_revision = :observed_revision;

Если пользователь уже изменил строку, условие не совпадёт: backfill перечитает её и повторно вычислит результат по актуальным данным. Нулевое число обновлённых строк требует разбирательства по правилам задания, а не отметки «перенесено». Удалённую запись также нельзя автоматически создавать заново. Подход работает только при выполнении соглашения о версиях всеми писателями.

Поведение конкурентного обновления зависит от СУБД и уровня изоляции. Например, PostgreSQL в Read Committed повторно проверяет условие WHERE для обновлённой конкурентом версии строки. В других режимах возможны ошибки сериализации и необходимость повтора транзакции; это включают в тесты выбранной реализации.

Альтернатива — короткая транзакция с подходящей блокировкой строки. Документация PostgreSQL о блокировках строк объясняет, что SELECT FOR UPDATE мешает конкурентным изменениям до завершения транзакции. Это помогает согласованию, но создаёт ожидания. Выбор механизма зависит от нагрузки; держать блокировки на большой пакет во время медленных внешних действий не следует.

Переносите небольшими порциями с возможностью продолжения

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

В учебной таблице 120 000 исторических записей. При условном размере пакета 500 получается 240 первоначальных пакетов. Это лишь разбиение работы: повторные попытки, конкурентные изменения и исключения увеличат число операций. По этому расчёту нельзя обещать срок завершения без измерения длительности и допустимой нагрузки.

Проверять нужно все подходящие записи, включая уже заполненные новым писателем: в период сосуществования A и B они могли устареть. Поэтому алгоритм «обновить только NULL» не заменяет сверку. Курсор последней обработанной записи тоже не доказывает полноту: команда проверяет согласованные границы набора, новые поступления и отложенные транзакции.

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

Подтвердите данные перед переключением чтения

Совпадение количества записей недостаточно. Проверяйте соответствие значений, допустимость идентификаторов, обязательность полей и сохранение бизнес-смысла. В нашем примере для каждого заказа текст courier должен соответствовать 10, а pickup — 20. Неизвестные варианты выделяются отдельно, а не исчезают из знаменателя отчёта.

Пусть в фиксированном учебном наборе из 120 000 строк сверка обнаружила 119 970 корректных пар, 20 незаполненных полей и 10 расхождений. Сумма равна 120 000, но переход ещё не готов: 30 записей требуют обработки. После устранения причины повторная проверка должна подтвердить каждую категорию, а не только обнулить счётчик ошибок задания.

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

После проверки данных оцените, когда вводить ограничения новой модели: обязательность значения, допустимость ссылок на справочник и другие правила. К этому моменту все активные писатели должны их соблюдать. Способ создания и проверки ограничения выбирают по документации используемой СУБД, снова учитывая блокировки и сканирование таблицы. Ошибка существующей записи не должна неожиданно превращать завершающий шаг в длительный отказ рабочих операций.

Переключайте чтение после проверки результата

Версия C начинает читать идентификатор, сохраняя оба представления при записи. Перед открытием нового пути можно сравнить ответы старого и нового чтения в контролируемом режиме, не выполняя бизнес-действие дважды. Проверяйте не только наличие результата, но и правильный способ доставки, расчёт и отображение для пользователя.

Временный возврат к старому полю при пустом новом значении иногда помогает совместимости, однако такой путь должен быть виден в наблюдениях. Если приложение постоянно обращается к старому источнику незаметно, зелёный экран скрывает незавершённую миграцию. Укажите, когда запасной путь разрешён, что считается ошибкой и какие условия позволят его убрать.

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

Матрица совместимости и границы rollback

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

ЭтапЧтение и записьУсловие следующего шагаГраница возврата
Добавлена новая структураA читает и пишет старое; B постепенно начинает писать оба поляВсе писатели A завершены, новые значения сверяютсяA допустима, но новое представление ещё нельзя считать актуальным
Перенос завершёнB читает старое, поддерживает оба поляПодтверждены полнота, соответствие и конкурентные измененияОстановка переноса сохраняет старый путь чтения
Чтение переведеноC читает новое, пишет оба поляСтарый путь больше не нужен для штатной работыВозврат к B возможен при сохранённой актуальности старого поля
Старая запись прекращенаD читает и пишет только новоеНет процессов и потребителей старой структурыВозврат к чтению старого поля уже требует новой сверки и синхронизации
Старое поле удаленоРаботают версии без обращения к немуПодтверждены приложение, обработчики и отчётыКод, требующий старый столбец, запускать нельзя

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

Удаляйте старую структуру отдельным подтверждённым шагом

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

До выпуска версии D проверьте ограничения и триггеры старого столбца. Например, оставшийся NOT NULL может запретить создание заказа, если новый код больше не заполняет прежнее поле. Подготовьте совместимое изменение этих зависимостей и проверьте вставку новых объектов, а не только обновление существующих.

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

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

Что согласовать перед запуском миграции

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

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

  • Определены преобразование данных, исключения и источник истины каждого этапа.
  • Найдены все писатели и читатели, включая фоновые задания и служебные скрипты.
  • Проверены точные DDL-команды, блокировки и поведение при отмене.
  • Испытаны согласованная запись, конкурентный backfill и продолжение после сбоя.
  • Подготовлены сверка полноты, контроль бизнес-смысла и условия переключения чтения.
  • Зафиксированы допустимые версии возврата и запрет преждевременного удаления полей.

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