$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогПишем миграции баз данных без сюрпризов: безопасные схемы изменения таблиц

Пишем миграции баз данных без сюрпризов: безопасные схемы изменения таблиц

$ sudo teach IT
·23 июля 2026 г.·10 мин·69
Пишем миграции баз данных без сюрпризов: безопасные схемы изменения таблиц

Научимcя менять структуру БД так, чтобы минимизировать блокировки: фазовые миграции, бэкапы, совместимые изменения и откаты.

Содержание
Как миграции превращаются в инцидентыБлокировки и ожидания (DDL lock)Разрыв совместимости «старое приложение ↔ новая схема»Репликация и лог-шиппингОтсутствие отката или «откат из другого мира»Принцип: думайте не «ALTER TABLE», а «состояния системы»Фазовые миграции: модель «добавить → заполнить → переключить → убрать»Шаг 1. Добавляем новый элемент в нейтральном режимеШаг 2. Заполняем данные батчамиШаг 3. Переключаем приложение на новый вариантШаг 4. Добавляем ограничения и индексы (только после готовности данных)Шаг 5. Убираем старое после полного переходаСовместимые изменения схемы: что можно делать без «стопа мира»Добавление колонок: чаще всего безопасноПереименование: опасность в смешанных версияхИзменение типа: часто необратимо и блокирующеУдаление колонок/ограничений: делайте только в концеИндексы и уникальность: скрытый источник блокировокСоздание индекса на большой таблицеУникальность и дедлайныОграничения (NOT NULL, CHECK, FK) без рискаNOT NULLCHECKВнешние ключиУправление блокировками: время ожидания и «границы» транзакцийОграничьте время ожидания DDLСледите за планами выполненияРезервные копии и точки восстановления: что именно бэкапитьПодход с точками восстановленияБэкап схемы vs бэкап данныхПроектирование отката: как сделать rollback реальным1) Схемный откат (rollback DDL)2) Данные (backfill/transform rollback)Типовые сценарии и безопасные рецептыСценарий A: добавляем новую колонку и меняем формулу расчётаСценарий B: переименовать поле для API/ORMСценарий C: смена типа (например, int → bigint)Сценарий D: добавление внешнего ключа в существующие данныеОперационная дисциплина: как выполнять миграции без хаосаТестируйте миграции как продуктИспользуйте миграционные окна и наблюдаемость

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

Хорошая новость: большинству проблем можно заранее поставить «ограждения». Плохая — универсальной кнопки «без сюрпризов» не существует: безопасность зависит от СУБД (PostgreSQL, MySQL, MS SQL), характера изменений и того, как приложение читает/пишет данные.

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

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


Как миграции превращаются в инциденты

Прежде чем перейти к стратегиям, разберём типовые источники «сюрпризов»:

Блокировки и ожидания (DDL lock)

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

  • рост latency на запросах;
  • очереди на приложении;
  • временами — deadlocks или таймауты клиентов.

Особенно опасны миграции, которые:

  • переписывают таблицу (например, меняют тип колонки или добавляют/удаляют столбцы с пересчётом);
  • требуют перестроения индексов;
  • выполняются «в один шаг» на большой таблице.

Разрыв совместимости «старое приложение ↔ новая схема»

Даже если СУБД справилась с DDL, остаётся слой приложения. Классика:

  • вы добавили колонку и поменяли бизнес-логику так, что старый код падает на чтении/записи;
  • вы переименовали колонку, но старые инстансы приложения ещё работают;
  • вы изменили тип/формат данных, а фоновые джобы или запросы используют старый тип.

В результате получаете ошибки на уровне ORM/SQL (например, column does not exist) или логические ошибки.

Репликация и лог-шиппинг

На реплицированных системах миграции могут:

  • существенно увеличить нагрузку на репликацию;
  • создать задержку (replication lag);
  • потребовать специфичных опций безопасности (например, управление lock_timeout, statement_timeout, контроль порядка миграций).

Отсутствие отката или «откат из другого мира»

Если миграция прошла частично, откат может оказаться невозможен или опасен:

  • вы удалили колонку, данные из которой нужны для восстановления;
  • вы модифицировали формат так, что старый код не умеет интерпретировать данные;
  • миграция была необратимой (например, разрушили историю).

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


Принцип: думайте не «ALTER TABLE», а «состояния системы»

Безопасная миграция — это не одна команда, а последовательность согласованных состояний:

  1. Состояние A: работает старое приложение + старая схема.
  2. Состояние B: обновили схему так, чтобы и старое, и новое приложение могли сосуществовать.
  3. Состояние C: обновили приложение до новой версии (которая понимает изменения).
  4. Состояние D: убрали временные элементы (старые колонки/таблицы/индексы) и зафиксировали «новый мир».

На практике это означает: вместо «удалить старую колонку и переименовать новую» делаем поэтапные операции, где переходы управляют совместимостью.


Фазовые миграции: модель «добавить → заполнить → переключить → убрать»

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

Шаг 1. Добавляем новый элемент в нейтральном режиме

Пример: нужно изменить тип/формат поля или добавить колонку под новую логику.

Правило: на этом шаге не ломаем старое приложение.

  • Колонка должна быть nullable или иметь дефолт, не влияющий на старую запись.
  • Индексы/ограничения — аккуратно: если они затронут большой объём данных, их можно отложить.

Пример (PostgreSQL):

code
-- 1) Добавляем новую колонку, не ломая существующие записи
ALTER TABLE orders
ADD COLUMN total_amount_cents bigint;

Почему nullable? Потому что старое приложение не знает про новую колонку и может не заполнять её.

Шаг 2. Заполняем данные батчами

Если необходимо перенести/преобразовать данные, делаем это отдельным процессом без долгих блокировок.

В PostgreSQL часто используют UPDATE с ограничением на количество строк, в несколько итераций.

Пример (условная батч-обработка):

code
-- 2) Заполняем батчами; примерная схема — зависит от вашей уникальности/индексов
WITH batch AS (
  SELECT id
  FROM orders
  WHERE total_amount_cents IS NULL
  ORDER BY id
  LIMIT 10000
)
UPDATE orders o
SET total_amount_cents = (o.total_amount * 100)::bigint
FROM batch
WHERE o.id = batch.id;

Такой подход:

  • уменьшает время одной транзакции;
  • ограничивает рост блокировок;
  • позволяет остановиться и вернуться к плану.

Шаг 3. Переключаем приложение на новый вариант

Когда данные подготовлены, обновляем код приложения.

  • В новом коде запись идёт в новую колонку (а при необходимости — ещё и в старую, чтобы сохранить консистентность в период смешанной версии).
  • Чтение — сначала безопасно переключаем через feature flag или поэтапно.

Если нужна двойная запись на период миграции, это особенно важно при rolling deploy (когда часть инстансов ещё старые).

Шаг 4. Добавляем ограничения и индексы (только после готовности данных)

Например, NOT NULL, CHECK, внешний ключ, уникальность. Эти изменения часто самые опасные из-за необходимости проверить данные.

По возможности:

  • сначала создайте индекс конкурентно (в PostgreSQL — CREATE INDEX CONCURRENTLY);
  • затем включайте ограничение, когда таблица уже заполнена корректно.

Пример:

code
-- 4) Когда данные заполнены, можно сделать NOT NULL
ALTER TABLE orders
ALTER COLUMN total_amount_cents SET NOT NULL;

-- Индекс
CREATE INDEX CONCURRENTLY idx_orders_total_amount_cents
ON orders (total_amount_cents);

Важно: в PostgreSQL CREATE INDEX CONCURRENTLY не требует длительных блокировок чтения, но требует аккуратности в управлении миграциями (например, транзакции).

Шаг 5. Убираем старое после полного перехода

Только когда:

  • вы обновили все инстансы приложения;
  • убедились по метрикам/логам, что старые пути не используются;
  • выдержали окно времени, чтобы все кэш-пересборки/фоновые джобы завершились,

можно удалять старые колонки/таблицы/индексы.


Совместимые изменения схемы: что можно делать без «стопа мира»

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

Добавление колонок: чаще всего безопасно

Добавить колонку (особенно nullable) обычно можно без драм. Главное — чтобы:

  • старое приложение игнорировало её (обычно оно делает INSERT списком колонок, а не INSERT ... VALUES (...) без списка);
  • не было триггеров/генераторов, которые внезапно начнут требовать значения.

Переименование: опасность в смешанных версиях

Переименование ломает старые запросы и ORM-схемы. В фазовой схеме лучше:

  1. добавить новую колонку;
  2. заполнить;
  3. обновить приложение;
  4. лишь потом (в отдельном релизном окне) убрать старую.

Если всё-таки нужно переименование, можно использовать view или синонимы, но в чистом виде это обычно сложнее и зависит от СУБД.

Изменение типа: часто необратимо и блокирующе

Изменение типа столбца может:

  • требовать перестройки данных;
  • вызвать долгую блокировку;
  • быть несовместимым на уровне приложения и драйверов.

Паттерн безопасного изменения типа:

  • добавить колонку нового типа;
  • заполнить преобразованием;
  • переключить чтение/запись;
  • после подтверждения — удалить старую.

Удаление колонок/ограничений: делайте только в конце

Удаление — это почти всегда необратимое действие в рамках смешанной версии. Даже если «в теории» старый код больше не использует колонку, прод — живой организм.


Индексы и уникальность: скрытый источник блокировок

Индексы кажутся безопасными, но на практике есть подводные камни.

Создание индекса на большой таблице

Обычное CREATE INDEX может блокировать запись или ухудшать нагрузку. В PostgreSQL:

  • используйте CREATE INDEX CONCURRENTLY, если вам критична минимизация блокировок;
  • помните, что команда не может выполняться внутри обычной транзакции.

Пример:

code
CREATE INDEX CONCURRENTLY idx_orders_customer_id
ON orders (customer_id);

Уникальность и дедлайны

Добавление UNIQUE:

  1. потребует проверки всех строк;
  2. может быть длительным;
  3. иногда ведёт к миграциям, которые «висели» у людей на проде.

Фазовая схема:

  • сначала создать уникальный индекс (если возможно и приемлемо);
  • обработать конфликты в данных;
  • только затем включить ALTER TABLE ... ADD CONSTRAINT.

Ограничения (NOT NULL, CHECK, FK) без риска

Чаще всего проблемы возникают, когда ограничение включают «сразу», не подготовив данные.

NOT NULL

Безопасный подход:

  1. добавьте колонку nullable;
  2. заполните;
  3. только затем SET NOT NULL.

Пример:

code
ALTER TABLE payments
ADD COLUMN provider_reference text;

-- заполняем батчами...
-- потом:
ALTER TABLE payments
ALTER COLUMN provider_reference SET NOT NULL;

CHECK

Если CHECK зависит от выражений или новых колонок:

  • проверьте, что все данные удовлетворяют условию;
  • включайте CHECK после backfill.

Внешние ключи

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

Фазовые варианты:

  • сначала добавьте колонку внешнего ключа nullable;
  • заполните и почистите;
  • затем добавьте constraint в конце.

Управление блокировками: время ожидания и «границы» транзакций

Даже фазовая миграция может стать опасной, если любая операция будет ждать слишком долго.

Ограничьте время ожидания DDL

В PostgreSQL полезно выставлять:

  • lock_timeout — сколько ждать блокировку;
  • statement_timeout — сколько длиться запросу;
  • размеры батчей — чтобы транзакции были короткими.

Пример:

code
SET lock_timeout = '5s';
SET statement_timeout = '30s';

И затем выполняйте DML/DDL в рамках этих ограничений. Если что-то не получилось — миграцию можно повторить, но вы не превращаете прод в «сервис ожиданий».

Следите за планами выполнения

Даже в батчах можно случайно построить дорогие запросы. Проверьте:

  • есть ли индексы на предикаты WHERE ... IS NULL/WHERE updated_at < ...;
  • используете ли вы корректный порядок.

Резервные копии и точки восстановления: что именно бэкапить

«Сделать бэкап» звучит просто, но важно уточнить:

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

Подход с точками восстановления

На PostgreSQL обычно применяют:

  • логические бэкапы (pg_dump) — для меньших систем или гибкой миграции схемы;
  • физические бэкапы (base backup) + PITR (point-in-time recovery) — когда важна точность отката по времени.

Если у вас включён репликационный журнал/WAL, PITR часто даёт наиболее предсказуемый откат «как было».

Бэкап схемы vs бэкап данных

Схема меняется миграциями. Для отката полезно иметь:

  • фиксацию версий миграций (какие номера применены);
  • возможность вернуться к предыдущей схеме;
  • автоматизацию развёртывания (чтобы не было «мы откатили таблицу руками»).

Проектирование отката: как сделать rollback реальным

Откат должен быть определён заранее. Есть два уровня отката:

1) Схемный откат (rollback DDL)

Если вы сделали только обратимые шаги — это легко. Но часто необратимость проявляется в:

  • удалении колонок;
  • изменении типов;
  • разрушении данных.

Поэтому в фазовой схеме откат обычно сводится к тому, чтобы:

  • вернуть приложение на старую схему;
  • оставить новые колонки как есть (они не мешают);
  • либо откатить дополнительные индексы/ограничения.

Иными словами: часто откат — это не “удалить всё”, а “отключить новую ветку”.

2) Данные (backfill/transform rollback)

Если вы заполняли новые колонки батчами, а потом решили откатиться:

  • можно просто перестать читать новую колонку и снова использовать старую;
  • новые данные остаются, но не вредят.

Если же вы меняли старую колонку (например, обновляли её в месте), откат может требовать восстановления из бэкапа или обратной трансформации.

Практическое правило: при возможности не трогайте старое до тех пор, пока не переключились полностью.


Типовые сценарии и безопасные рецепты

Рассмотрим несколько распространённых «боевых» кейсов и разложим их на безопасные шаги.

Сценарий A: добавляем новую колонку и меняем формулу расчёта

Цель: вместо total_amount используем total_amount_cents с другой точностью/округлением.

Шаги:

  1. Добавить новую колонку total_amount_cents nullable.
  2. Backfill.
  3. Обновить приложение: запись/чтение по новой колонке (можно в период перехода писать в обе).
  4. SET NOT NULL.
  5. Удалить старую колонку в конце.

Критичные моменты:

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

Сценарий B: переименовать поле для API/ORM

Если приложение использует ORM-метаданные, переименование ломает маппинги.

Решение: не переименовывайте сразу на уровне таблицы.

  • Добавьте новую колонку (или создайте view);
  • заполните;
  • обновите ORM маппинг;
  • через время удалите старую.

Если есть возможность — view может служить мостом, но это усложняет производительность и планы запросов.

Сценарий C: смена типа (например, int → bigint)

Менять тип «в лоб» опасно. Лучше:

  • добавить новую колонку нового типа;
  • backfill;
  • переключить приложение;
  • добавить ограничения;
  • удалить старую колонку.

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

Сценарий D: добавление внешнего ключа в существующие данные

Сначала создайте колонку nullable для FK. Затем:

  • очистите/приведите данные, чтобы они соответствовали ссылочной таблице;
  • только потом добавляйте constraint.

Операционная дисциплина: как выполнять миграции без хаоса

Техническая схема — половина успеха. Вторая половина — процесс.

Тестируйте миграции как продукт

Проверьте:

  • миграцию на стенде с данными, близкими к реальным (объём и распределение);
  • время выполнения и влияние на индекс/план запросов;
  • поведение под конкурентной нагрузкой.

Если вы не можете поднять такую нагрузку — хотя бы смоделируйте «тяжёлые» запросы и профилируйте.

Используйте миграционные окна и наблюдаемость

В проде вам нужны ответы на вопросы:

  • что происходит с latency/throughput во время DDL?
  • растёт ли блокировочный счётчик?
  • падает ли процент успешных запросов?
  • увеличилась ли задержка репликации?

В PostgreSQL полезно мониторить:

  • pg_stat_activity (блокировки/ожидания);
  • метрики блокировок/дедлоков (в зависимости от системы

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

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

Перейти к курсу
Приложения для iPhone и Apple Watch на SwiftUI

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Приложения для macOS на SwiftUI

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API
модели

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API

Разберём, как описывать доменные сущности, DTO и error-model на уровне типов, чтобы ловить несовпадения схем до продакшена. Покажем практические шаблоны для типобезопасного маппинга и версионирования контрактов.

15 сентября 2026 г.
1040
Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck
деплой

Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck

Разберём практический чек-лист подготовки контейнера к продакшену: как собрать воспроизводимый образ, как правильно прокидывать конфиги/секреты через окружение и как настроить healthcheck, чтобы деплой не “успевал” раньше готовности сервиса.

18 сентября 2026 г.
1030
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1421
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

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

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Как миграции превращаются в инцидентыБлокировки и ожидания (DDL lock)Разрыв совместимости «старое приложение ↔ новая схема»Репликация и лог-шиппингОтсутствие отката или «откат из другого мира»Принцип: думайте не «ALTER TABLE», а «состояния системы»Фазовые миграции: модель «добавить → заполнить → переключить → убрать»Шаг 1. Добавляем новый элемент в нейтральном режимеШаг 2. Заполняем данные батчамиШаг 3. Переключаем приложение на новый вариантШаг 4. Добавляем ограничения и индексы (только после готовности данных)Шаг 5. Убираем старое после полного переходаСовместимые изменения схемы: что можно делать без «стопа мира»Добавление колонок: чаще всего безопасноПереименование: опасность в смешанных версияхИзменение типа: часто необратимо и блокирующеУдаление колонок/ограничений: делайте только в концеИндексы и уникальность: скрытый источник блокировокСоздание индекса на большой таблицеУникальность и дедлайныОграничения (NOT NULL, CHECK, FK) без рискаNOT NULLCHECKВнешние ключиУправление блокировками: время ожидания и «границы» транзакцийОграничьте время ожидания DDLСледите за планами выполненияРезервные копии и точки восстановления: что именно бэкапитьПодход с точками восстановленияБэкап схемы vs бэкап данныхПроектирование отката: как сделать rollback реальным1) Схемный откат (rollback DDL)2) Данные (backfill/transform rollback)Типовые сценарии и безопасные рецептыСценарий A: добавляем новую колонку и меняем формулу расчётаСценарий B: переименовать поле для API/ORMСценарий C: смена типа (например, int → bigint)Сценарий D: добавление внешнего ключа в существующие данныеОперационная дисциплина: как выполнять миграции без хаосаТестируйте миграции как продуктИспользуйте миграционные окна и наблюдаемость