Миграции БД
Миграция базы данных — версионированное изменение её схемы, описанное кодом и применяемое автоматически на всех окружениях.
Что такое миграция базы данных
Миграция БД — описанное в коде изменение схемы базы: добавить таблицу, колонку, индекс, поменять тип поля. Каждая миграция — отдельный файл, лежащий в репозитории рядом с приложением и применяемый автоматически.
Смысл в том, что схема базы становится частью кода. Разработчик добавил колонку у себя, закоммитил миграцию — и на тестовом стенде и в проде она применится тем же кодом, в том же порядке. Альтернатива — ручные ALTER TABLE в консоли прода — заканчивается предсказуемо: схемы на окружениях расходятся, а что и когда меняли, не помнит никто.
Не путать с двумя другими значениями. «Миграция БД» иногда означает перенос базы на другой сервер или переезд с одной СУБД на другую (например, с MS SQL Server на PostgreSQL). Здесь речь про версионирование схемы.
Как это устроено
migrations/
2026_07_01_120000_create_users_table.php
2026_07_03_093000_add_phone_to_users.php
2026_07_10_141500_create_orders_table.php
Инструмент хранит в самой базе служебную таблицу с применёнными миграциями:
SELECT * FROM migrations;
-- id | migration | batch
-- ----+--------------------------------------------+-------
-- 1 | 2026_07_01_120000_create_users_table | 1
-- 2 | 2026_07_03_093000_add_phone_to_users | 1
При запуске он сравнивает список файлов с этой таблицей и применяет недостающие по порядку. Отсюда — идемпотентность: команду можно вызывать сколько угодно раз, повторно ничего не применится.
Up и down
Миграция описывает два направления: как применить изменение и как его откатить.
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone', 20)->nullable()->index();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone');
});
}
Честная оговорка про откат: down хорошо работает на тестовом стенде и плохо — на проде. Откат DROP COLUMN вернёт схему, но не данные, которые в этой колонке лежали. Поэтому на бою деструктивные изменения чаще исправляют новой миграцией «вперёд», а не откатом назад, а перед накаткой снимают дамп.
Инструменты
| Инструмент | Экосистема | Формат |
|---|---|---|
| Laravel Migrations | PHP | PHP-классы |
| Alembic | Python / SQLAlchemy | Python |
| Flyway | Java и не только | SQL-файлы |
| Liquibase | Java и не только | XML/YAML/SQL |
| Knex, Prisma | Node.js | JS/TS |
| golang-migrate | Go | SQL-файлы |
| django ORM | Python | Python, автогенерация |
Разделение простое: у фреймворков — свои встроенные, вне фреймворка — Flyway и Liquibase на чистом SQL.
Опасные операции
Главная ловушка — миграция, которая мгновенно проходит на пустой базе разработчика и кладёт прод на 20 минут:
ALTER TABLE ... ADD COLUMNсо значением по умолчанию. В современных PostgreSQL и MySQL это дёшево, в старых версиях — переписывание всей таблицы под блокировкой.CREATE INDEXблокирует запись в таблицу на всё время построения. В PostgreSQL спасаетCREATE INDEX CONCURRENTLY— дольше, но без блокировки.- Смена типа колонки обычно означает полное переписывание таблицы.
ADD CONSTRAINTс проверкой сканирует таблицу целиком. В PostgreSQL добавляют какNOT VALID, затем отдельноVALIDATE CONSTRAINT.NOT NULLна существующую колонку упадёт, если в ней естьNULL.
-- Плохо на живой таблице: блокирует запись
CREATE INDEX idx_orders_user ON orders (user_id);
-- Хорошо: без блокировки, вне транзакции
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);
-- Ограничение без сканирования всей таблицы
ALTER TABLE orders ADD CONSTRAINT ck_total CHECK (total >= 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT ck_total;
Отдельно про блокировки: ALTER TABLE ждёт завершения текущих запросов к таблице и при этом уже держит очередь на блокировку — за ним копятся все новые запросы. Одна долгая транзакция превращает «мгновенную» миграцию в остановку сервиса. Поэтому на PostgreSQL ставят lock_timeout и повторяют попытку, а не ждут бесконечно.
Zero-downtime и expand-contract
Во время деплоя старый и новый код работают одновременно, поэтому схема обязана быть совместима с обоими. Отсюда правило: миграция и код выкатываются разными шагами. Переименование колонки одной миграцией гарантированно ломает старые экземпляры приложения.
Приём expand-contract на примере переименования phone → phone_number:
- Expand. Добавить
phone_number, писать в обе колонки. Старый код продолжает работать. - Backfill. Перелить данные пачками, а не одним
UPDATEна всю таблицу. - Migrate. Выкатить код, который читает новую колонку.
- Contract. Убедиться, что старая не используется, и удалить её отдельной миграцией.
Правила эксплуатации
- Не редактировать применённую миграцию. У других она уже накатилась, и правка файла до них не доедет — только новая миграция.
- Одна миграция — одно изменение. Проще откатывать и читать в истории.
- Накатывать в CI/CD отдельным шагом перед выкладкой кода, а не руками по SSH.
- Backfill данных — не в миграции.
UPDATEна миллионы строк внутри миграции блокирует деплой; такие вещи делают фоновой задачей пачками. - Проверять на копии прода. Миграция, отработавшая за 0.1 с на 100 строках, на 100 млн строк ведёт себя иначе.
Связанные термины
- Базы данных — то, чью схему меняют миграции
- SQL — DDL, в который превращается миграция
- Ключи в БД — ограничения, добавляемые миграциями
- Индексы в БД —
CREATE INDEX CONCURRENTLY - Дамп базы данных — страховка перед накаткой
- CI/CD — где миграции применяются автоматически
Готовы запустить GPU-задачу?
Запустить GPU-сервер