Databases

Миграции БД

Миграция базы данных — версионированное изменение её схемы, описанное кодом и применяемое автоматически на всех окружениях.

Что такое миграция базы данных

Миграция БД — описанное в коде изменение схемы базы: добавить таблицу, колонку, индекс, поменять тип поля. Каждая миграция — отдельный файл, лежащий в репозитории рядом с приложением и применяемый автоматически.

Смысл в том, что схема базы становится частью кода. Разработчик добавил колонку у себя, закоммитил миграцию — и на тестовом стенде и в проде она применится тем же кодом, в том же порядке. Альтернатива — ручные 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 на примере переименования phonephone_number:

  1. Expand. Добавить phone_number, писать в обе колонки. Старый код продолжает работать.
  2. Backfill. Перелить данные пачками, а не одним UPDATE на всю таблицу.
  3. Migrate. Выкатить код, который читает новую колонку.
  4. Contract. Убедиться, что старая не используется, и удалить её отдельной миграцией.

Правила эксплуатации

  • Не редактировать применённую миграцию. У других она уже накатилась, и правка файла до них не доедет — только новая миграция.
  • Одна миграция — одно изменение. Проще откатывать и читать в истории.
  • Накатывать в CI/CD отдельным шагом перед выкладкой кода, а не руками по SSH.
  • Backfill данных — не в миграции. UPDATE на миллионы строк внутри миграции блокирует деплой; такие вещи делают фоновой задачей пачками.
  • Проверять на копии прода. Миграция, отработавшая за 0.1 с на 100 строках, на 100 млн строк ведёт себя иначе.

Связанные термины

Готовы запустить GPU-задачу?

Запустить GPU-сервер