Миграции базы данных: как безопасно менять схему
Практика3 минРедакция Innovialabs
Коротко
Меняйте схему только через пронумерованные миграции в репозитории, применяйте их автоматически при деплое и разбивайте опасные изменения на совместимые шаги, чтобы старый и новый код работали с базой одновременно.
Короткий ответ
Миграция — это файл с изменением схемы (создать таблицу, добавить колонку, индекс), у которого есть номер версии. Инструмент хранит в базе служебную таблицу с уже применёнными версиями и при запуске выполняет только новые файлы по порядку.
Правила безопасной работы:
- схему никогда не меняют руками на сервере — только через миграцию в Git;
- применённую миграцию не редактируют, а пишут новую;
- одна и та же цепочка миграций прогоняется на dev, staging и production;
- опасные изменения разбиваются на несколько шагов.
Какой инструмент выбрать
| Инструмент | Экосистема | Как описываются миграции |
|---|---|---|
| Flyway | Java, любая через CLI | SQL-файлы V1__init.sql, V2__add_orders.sql |
| Liquibase | Java, любая через CLI | changelog в XML/YAML/JSON или SQL, есть встроенные rollback-блоки |
| Prisma Migrate | Node.js / TypeScript | меняете schema.prisma, инструмент генерирует SQL |
| Alembic | Python / SQLAlchemy | Python-файлы с функциями upgrade() и downgrade() |
Выбирайте инструмент вашего стека: ORM-проекты обычно используют родной механизм (Prisma, Alembic, миграции Django или Laravel). Flyway и Liquibase удобны, когда база общая для нескольких сервисов на разных языках.
Пример Alembic-миграции:
def upgrade():
op.add_column("users", sa.Column("phone", sa.String(20), nullable=True))
def downgrade():
op.drop_column("users", "phone")
Как менять схему без простоя
Во время деплоя какое-то время работают и старая, и новая версия приложения. Поэтому каждая миграция должна быть обратно совместимой со старым кодом. Для этого используют схему expand / contract.
Пример: переименовать колонку name в full_name.
- Expand. Добавьте новую колонку
full_name, допускающую NULL. - Код пишет в обе колонки, читает из старой.
- Отдельной задачей перенесите старые данные пачками.
- Код начинает читать из
full_name. - Contract. Следующим релизом удалите колонку
name.
Что опасно делать одной миграцией:
- переименование или удаление колонки, которую читает текущий код;
NOT NULLна большой таблице без значения по умолчанию;- изменение типа колонки, требующее перезаписи таблицы;
- создание индекса с блокировкой записи — в PostgreSQL используйте
CREATE INDEX CONCURRENTLY.
Стратегии отката
Есть два подхода, и они не взаимоисключающие.
- Down-миграции (
downgrade(), rollback в Liquibase). Удобны на dev, но на production откат удаления колонки не вернёт данные. - Roll forward. Ошибку исправляют новой миграцией. Для production это чаще надёжнее.
Минимальная страховка: бэкап перед каждой рискованной миграцией и проверенный план, что делать, если миграция упала посередине. Учтите, что в MySQL DDL-операции не откатываются транзакцией, а в PostgreSQL большинство DDL транзакционны.
Ревью миграций
Миграцию проверяют внимательнее обычного кода, потому что её нельзя просто «откатить коммитом». Чеклист для ревьюера:
- совместима ли миграция с кодом, который сейчас в production;
- сколько строк в таблице и будет ли долгая блокировка;
- есть ли перенос данных внутри DDL-миграции (лучше вынести отдельно);
- сгенерированный ORM SQL прочитан глазами, а не принят на веру;
- миграция прогнана на копии с реалистичным объёмом данных.
Как держать окружения в синхроне
- Миграции применяются автоматически в CI/CD перед запуском новой версии, а не вручную.
- В CI поднимается чистая база и прогоняются все миграции с нуля — так ловятся сломанные цепочки.
- Включите проверку дрейфа: Flyway
validate, Prismamigrate diffи аналоги показывают расхождение между файлами и реальной схемой. - Не допускайте ручных правок схемы на production: если они случились, оформите их миграцией.
Частые ошибки
- Правка уже применённой миграции — контрольные суммы не сходятся, окружения расходятся.
- Два разработчика создали миграции с одинаковым номером в разных ветках.
- Миграция и деплой кода, который от неё зависит, выкатываются в неправильном порядке.
- Перенос миллионов строк одним
UPDATEвнутри миграции.