О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Постановка задачи

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

Принципы миграций

ПринципОписание
Backward CompatibleСтарая версия приложения работает с новой схемой
Forward CompatibleНовая версия приложения работает со старой схемой (во время rollout)
ReversibleДля каждой миграции есть скрипт отката
IdempotentПовторный запуск миграции даёт тот же результат
Small & IncrementalПредпочитайте много небольших миграций вместо изменений «одним большим взрывом»

Безопасные паттерны миграций

1. Добавление колонки (безопасно)

-- Миграция: add_email_verified_column
-- Безопасно: новая колонка со значением по умолчанию, чтение от неё пока не зависит
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT false;
-- Код приложения можно задеплоить до или после
-- Старый код игнорирует новую колонку, новый код её использует

2. Переименование колонки (в несколько шагов)

Шаг 1: Добавить новую колонку

-- Миграция 001: add_full_name_column
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

Шаг 2: Заполнить данные (backfill)

-- Миграция 002: backfill_full_name
UPDATE users SET full_name = CONCAT(first_name, ' ', last_name) WHERE full_name IS NULL;

Шаг 3: Задеплоить код, который пишет в обе и читает из новой

// Версия кода 2: двойная запись
class User extends Model
{
protected $fillable = ['first_name', 'last_name', 'full_name'];
public function save(array $options = [])
{
// Писать в обе во время перехода
if ($this->isDirty('first_name') || $this->isDirty('last_name')) {
$this->full_name = $this->first_name . ' ' . $this->last_name;
}
return parent::save($options);
}
public function getDisplayNameAttribute()
{
// Читать из новой колонки; при необходимости — fallback на старую
return $this->full_name ?? ($this->first_name . ' ' . $this->last_name);
}
}

Шаг 4: Удалить старые колонки (после того, как все pods на новом коде)

-- Миграция 003: remove_old_name_columns (запускать после полного деплоя)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;

3. Добавление ограничения NOT NULL (в несколько шагов)

-- Шаг 1: добавить колонку как nullable
ALTER TABLE orders ADD COLUMN customer_id INTEGER;
-- Шаг 2: выполнить backfill существующих строк
UPDATE orders SET customer_id = (
SELECT id FROM customers WHERE customers.email = orders.customer_email
);
-- Шаг 3: добавить ограничение (после завершения backfill)
ALTER TABLE orders ALTER COLUMN customer_id SET NOT NULL;
-- Шаг 4: добавить внешний ключ
ALTER TABLE orders ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id);

4. Удаление колонки (безопасная последовательность)

// Шаг 1: прекратить читать колонку в коде
// Удалить все SELECT-запросы, которые включают эту колонку
// Шаг 2: прекратить писать в колонку
// Удалить все INSERT/UPDATE-операторы, которые устанавливают эту колонку
// Шаг 3: задеплоить и убедиться, что ошибок нет
// Шаг 4: удалить колонку (миграция)
-- Миграция: drop_legacy_status_column
-- Запускать только после деплоя изменений кода
ALTER TABLE orders DROP COLUMN legacy_status;

Инструменты миграций, специфичные для framework

Laravel Migrations

// database/migrations/2024_01_15_create_orders_table.php
class CreateOrdersTable extends Migration
{
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('status')->default('pending');
$table->decimal('total', 10, 2);
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'status']);
});
}
public function down()
{
Schema::dropIfExists('orders');
}
}
// Запустить миграции
php artisan migrate
// Откатить последний batch
php artisan migrate:rollback
// Откатить конкретные шаги
php artisan migrate:rollback --step=3

Ruby on Rails Migrations

# db/migrate/20240115_add_tracking_to_orders.rb
class AddTrackingToOrders < ActiveRecord::Migration[7.0]
# Отключить транзакцию для больших таблиц
disable_ddl_transaction!
def change
# Добавить колонку с concurrent index
add_column :orders, :tracking_number, :string
add_index :orders, :tracking_number,
algorithm: :concurrently,
if_not_exists: true
end
end
# Запустить миграции
rails db:migrate
# Откат
rails db:rollback STEP=1

Django Migrations

# orders/migrations/0002_add_tracking.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='order',
name='tracking_number',
field=models.CharField(max_length=100, null=True, blank=True),
),
migrations.AddIndex(
model_name='order',
index=models.Index(fields=['tracking_number'], name='tracking_idx'),
),
]
# Запустить миграции
python manage.py migrate
# Создать миграцию на основе изменений модели
python manage.py makemigrations
# Откат
python manage.py migrate orders 0001

Миграции больших таблиц

Online Schema Change с pt-online-schema-change (MySQL)

# Добавить колонку в большую таблицу без блокировок
pt-online-schema-change \
--alter "ADD COLUMN new_field VARCHAR(255)" \
--execute \
--host=localhost \
--user=root \
--ask-pass \
D=mydb,t=large_table

Concurrent Index в PostgreSQL

-- Создать индекс без блокировки записей
CREATE INDEX CONCURRENTLY idx_orders_created ON orders(created_at);
-- Если не удалось — сначала удалите некорректный индекс
DROP INDEX CONCURRENTLY IF EXISTS idx_orders_created;

Batch-обновления для больших таблиц

// Laravel batch update
class BackfillUserSettings extends Migration
{
public function up()
{
// Обрабатывать пакетами, чтобы избежать проблем с памятью
User::query()
->whereNull('settings')
->chunkById(1000, function ($users) {
foreach ($users as $user) {
$user->update(['settings' => json_encode(['theme' => 'light'])]);
}
});
}
}
# Rails batch update
class BackfillUserSettings < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def up
User.where(settings: nil).find_in_batches(batch_size: 1000) do |batch|
User.where(id: batch.map(&:id)).update_all(settings: '{"theme": "light"}')
sleep(0.1) # Снизить нагрузку на базу данных
end
end
end

Миграции в CI/CD pipeline

GitLab CI job для миграций

migrate-database:
stage: deploy
image: myapp:${CI_COMMIT_SHA}
script:
# Проверить наличие ожидающих миграций
- php artisan migrate:status
# Запустить миграции
- php artisan migrate --force
# Проверить миграцию
- php artisan migrate:status | grep -q "No pending migrations"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
changes:
- database/migrations/**/*
environment:
name: production

Kubernetes Job для миграций

apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-${CI_COMMIT_SHA}
namespace: production
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
initContainers:
- name: wait-for-db
image: busybox
command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 2; done']
containers:
- name: migrate
image: myapp:${CI_COMMIT_SHA}
command: ["php", "artisan", "migrate", "--force"]
envFrom:
- secretRef:
name: app-secrets

Стратегии отката миграций

Поддержка отката на уровне приложения

// Feature flag для отката
class OrderController extends Controller
{
public function store(Request $request)
{
$order = new Order();
$order->user_id = auth()->id();
$order->total = $request->total;
// Новое поле с feature flag
if (Feature::active('new-order-tracking')) {
$order->tracking_number = $this->generateTracking();
}
$order->save();
return response()->json($order);
}
}

Снимок базы данных перед миграцией

#!/bin/bash
# pre-migration-backup.sh
set -e
BACKUP_NAME="pre-migration-$(date +%Y%m%d-%H%M%S)"
# Создать snapshot
pg_dump $DATABASE_URL > /tmp/${BACKUP_NAME}.sql
# Загрузить в S3
aws s3 cp /tmp/${BACKUP_NAME}.sql s3://db-backups/migrations/${BACKUP_NAME}.sql
# Сохранить имя backup для потенциального отката
echo $BACKUP_NAME > /tmp/last-migration-backup
echo "Backup created: $BACKUP_NAME"

Тестирование миграций

Тестируйте миграции в CI

test-migrations:
stage: test
services:
- postgres:15
variables:
DATABASE_URL: postgres://test:test@postgres:5432/test
script:
# Применить все миграции
- php artisan migrate --force
# Запустить down-миграции
- php artisan migrate:rollback --step=999
# Снова запустить up
- php artisan migrate --force
# Заполнить данными (seed) и проверить
- php artisan db:seed
- php artisan test --filter=DatabaseTest

Тестирование на shadow database

test-migration-on-copy:
stage: pre-deploy
script:
# Создать копию production базы данных
- pg_dump $PROD_DATABASE_URL | psql $SHADOW_DATABASE_URL
# Запустить миграцию на shadow
- DATABASE_URL=$SHADOW_DATABASE_URL php artisan migrate --force
# Запустить smoke-тесты
- DATABASE_URL=$SHADOW_DATABASE_URL php artisan test --filter=SmokeTest
# Удалить shadow database
- psql $SHADOW_DATABASE_URL -c "DROP DATABASE shadow_db"

Чек-лист миграции

До миграции

  • [ ] Подтвердить владение данными и зависимости
  • [ ] Миграция протестирована в staging-окружении с данными, близкими к production
  • [ ] Скрипт отката протестирован
  • [ ] Резервная копия базы данных создана и проверена через тренировочное восстановление
  • [ ] Команда уведомлена об окне миграции
  • [ ] Дашборды мониторинга готовы

Во время миграции

  • [ ] Мониторить CPU и I/O базы данных
  • [ ] Мониторить уровень ошибок приложения
  • [ ] Мониторить задержки запросов
  • [ ] Мониторить время ожидания блокировок
  • [ ] Проверять lag репликации (если применимо)

После миграции

  • [ ] Убедиться, что миграция завершилась успешно
  • [ ] Запустить smoke-тесты
  • [ ] Проверить логи приложения на ошибки
  • [ ] Запустить reconciliation-проверки (сравнить количество записей, суммы или хэши)
  • [ ] Обновить документацию
  • [ ] Очистить старые колонки/таблицы (после льготного периода)

Мышление Senior Database Engineer

Senior-инженеры относятся к базам данных как к критически важной инфраструктуре. Они инвестируют в:

  • Backups и тренировочные восстановления — никогда не запускайте крупную миграцию без проверенного backup
  • Ясность владения данными — перед изменением базы данных определите, какая система владеет каждым набором данных
  • Безопасные паттерны эволюции схемы — аддитивные, backward-compatible шаги с точками отката
  • Reconciliation-проверки — автоматизированное сравнение количества записей и целостности данных

Распространённые ошибки, которых следует избегать

  • Запуск крупных миграций без протестированных backup
  • Внесение ломающих изменений схемы без версионирования
  • Игнорирование lag репликации во время тяжёлых backfill
  • Добавление индексов без проверки влияния на запросы
  • Отсутствие документации по шагам миграции

Относитесь к миграциям как к production-изменениям, а не как к разовым скриптам. Когда операции с базой данных предсказуемы и безопасны, всю систему становится проще развивать.

Связанные статьи в Wiki

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив