Database Migration Patterns
Освойте миграции баз данных без простоев с Claude Code. Изучите безопасные изменения схем для PostgreSQL, Prisma, Django и других технологий, используя паттерн expand-contract.
Этот навык предоставляет комплексную структуру для управления изменениями схемы и данных в базе данных с акцентом на безопасность и развертывание без простоев. Он предлагает специализированные рекомендации для PostgreSQL и MySQL, а также конкретные рабочие процессы для популярных ORM, таких как Prisma, Drizzle и Django. Используя шаблон «expand-contract» и строгие контрольные списки безопасности, разработчики могут выполнять сложные операции — такие как переименование столбцов или добавление индексов на огромные таблицы — без блокировки рабочих баз данных или сбоев приложений.
Ключевые возможности
Примеры использования
| name | database-migrations |
|---|---|
| description | Şema değişiklikleri, veri migration'ları, rollback'ler ve PostgreSQL, MySQL ve yaygın ORM'ler (Prisma, Drizzle, Django, TypeORM, golang-migrate) arasında sıfır kesinti deployment'ları için veritabanı migration en iyi uygulamaları. |
| origin | ECC |
Üretim sistemleri için güvenli, geri alınabilir veritabanı şema değişiklikleri.
- Veritabanı tabloları oluştururken veya değiştirirken
- Sütun veya indeks eklerken/kaldırırken
- Veri migration'ları çalıştırırken (backfill, dönüştürme)
- Sıfır kesinti şema değişiklikleri planlarken
- Yeni bir proje için migration araçları kurarken
- Her değişiklik bir migration'dır — üretim veritabanlarını asla manuel olarak değiştirmeyin
- Migration'lar üretimde sadece ileri — rollback'ler yeni forward migration'lar kullanır
- Şema ve veri migration'ları ayrıdır — tek migration'da DDL ve DML'yi asla karıştırmayın
- Migration'ları üretim boyutundaki veriye karşı test edin — 100 satırda çalışan migration 10M'de kilitlenebilir
- Migration'lar üretimde çalıştıktan sonra değişmezdir — üretimde çalışan migration'ı asla düzenlemeyin
Herhangi bir migration uygulamadan önce:
- Migration UP ve DOWN'a sahip (veya açıkça geri alınamaz olarak işaretlenmiş)
- Büyük tablolarda tam tablo kilitleri yok (concurrent operasyonlar kullan)
- Yeni sütunlar varsayılanlara sahip veya nullable (varsayılan olmadan NOT NULL asla ekleme)
- İndeksler concurrent oluşturuluyor (mevcut tablolar için CREATE TABLE ile inline değil)
- Veri backfill şema değişikliğinden ayrı bir migration
- Üretim verisinin kopyasına karşı test edilmiş
- Rollback planı dokümante edilmiş
-- İYİ: Nullable sütun, kilit yok
ALTER TABLE users ADD COLUMN avatar_url TEXT;
-- İYİ: Varsayılanlı sütun (Postgres 11+ anlık, yeniden yazma yok)
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true;
-- KÖTÜ: Mevcut tabloda varsayılansız NOT NULL (tam yeniden yazma gerektirir)
ALTER TABLE users ADD COLUMN role TEXT NOT NULL;
-- Bu tabloyu kilitler ve her satırı yeniden yazar-- KÖTÜ: Büyük tablolarda yazmaları engeller
CREATE INDEX idx_users_email ON users (email);
-- İYİ: Engellemez, concurrent yazmalara izin verir
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
-- Not: CONCURRENTLY transaction bloğu içinde çalıştırılamaz
-- Çoğu migration aracı bunun için özel işleme ihtiyaç duyarÜretimde asla doğrudan yeniden adlandırmayın. Expand-contract kalıbını kullanın:
-- Adım 1: Yeni sütun ekle (migration 001)
ALTER TABLE users ADD COLUMN display_name TEXT;
-- Adım 2: Veriyi backfill et (migration 002, veri migration'ı)
UPDATE users SET display_name = username WHERE display_name IS NULL;
-- Adım 3: Uygulama kodunu her iki sütunu okuma/yazma için güncelle
-- Uygulama değişikliklerini deploy et
-- Adım 4: Eski sütuna yazmayı durdur, kaldır (migration 003)
ALTER TABLE users DROP COLUMN username;-- Adım 1: Sütuna tüm uygulama referanslarını kaldır
-- Adım 2: Sütun referansı olmadan uygulamayı deploy et
-- Adım 3: Sonraki migration'da sütunu kaldır
ALTER TABLE orders DROP COLUMN legacy_status;
-- Django için: SeparateDatabaseAndState kullanarak modelden kaldır
-- DROP COLUMN oluşturmadan (sonra sonraki migration'da kaldır)-- KÖTÜ: Tüm satırları tek transaction'da günceller (tabloyu kilitler)
UPDATE users SET normalized_email = LOWER(email);
-- İYİ: İlerleme ile batch güncelleme
DO $$
DECLARE
batch_size INT := 10000;
rows_updated INT;
BEGIN
LOOP
UPDATE users
SET normalized_email = LOWER(email)
WHERE id IN (
SELECT id FROM users
WHERE normalized_email IS NULL
LIMIT batch_size
FOR UPDATE SKIP LOCKED
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
RAISE NOTICE 'Updated % rows', rows_updated;
EXIT WHEN rows_updated = 0;
COMMIT;
END LOOP;
END $$;# Şema değişikliklerinden migration oluştur
npx prisma migrate dev --name add_user_avatar
# Üretimde bekleyen migration'ları uygula
npx prisma migrate deploy
# Veritabanını sıfırla (sadece dev)
npx prisma migrate reset
# Şema değişikliklerinden sonra client oluştur
npx prisma generatemodel User {
id String @id @default(cuid())
email String @unique
name String?
avatarUrl String? @map("avatar_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orders Order[]
@@map("users")
@@index([email])
}Prisma'nın ifade edemediği operasyonlar için (concurrent indeksler, veri backfill'leri):
# Boş migration oluştur, sonra SQL'i manuel düzenle
npx prisma migrate dev --create-only --name add_email_index-- migrations/20240115_add_email_index/migration.sql
-- Prisma CONCURRENTLY oluşturamaz, bu yüzden manuel yazıyoruz
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users (email);# Şema değişikliklerinden migration oluştur
npx drizzle-kit generate
# Migration'ları uygula
npx drizzle-kit migrate
# Şemayı doğrudan push et (sadece dev, migration dosyası yok)
npx drizzle-kit pushimport { pgTable, text, timestamp, uuid, boolean } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: text("email").notNull().unique(),
name: text("name"),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});# Model değişikliklerinden migration oluştur
python manage.py makemigrations
# Migration'ları uygula
python manage.py migrate
# Migration durumunu göster
python manage.py showmigrations
# Özel SQL için boş migration oluştur
python manage.py makemigrations --empty app_name -n descriptionfrom django.db import migrations
def backfill_display_names(apps, schema_editor):
User = apps.get_model("accounts", "User")
batch_size = 5000
users = User.objects.filter(display_name="")
while users.exists():
batch = list(users[:batch_size])
for user in batch:
user.display_name = user.username
User.objects.bulk_update(batch, ["display_name"], batch_size=batch_size)
def reverse_backfill(apps, schema_editor):
pass # Veri migration'ı, geri alma gerekmez
class Migration(migrations.Migration):
dependencies = [("accounts", "0015_add_display_name")]
operations = [
migrations.RunPython(backfill_display_names, reverse_backfill),
]# Migration çifti oluştur
migrate create -ext sql -dir migrations -seq add_user_avatar
# Tüm bekleyen migration'ları uygula
migrate -path migrations -database "$DATABASE_URL" up
# Son migration'ı rollback et
migrate -path migrations -database "$DATABASE_URL" down 1
# Versiyonu zorla (dirty durumu düzelt)
migrate -path migrations -database "$DATABASE_URL" force VERSION-- migrations/000003_add_user_avatar.up.sql
ALTER TABLE users ADD COLUMN avatar_url TEXT;
CREATE INDEX CONCURRENTLY idx_users_avatar ON users (avatar_url) WHERE avatar_url IS NOT NULL;
-- migrations/000003_add_user_avatar.down.sql
DROP INDEX IF EXISTS idx_users_avatar;
ALTER TABLE users DROP COLUMN IF EXISTS avatar_url;Kritik üretim değişiklikleri için expand-contract kalıbını takip edin:
Faz 1: EXPAND
- Yeni sütun/tablo ekle (nullable veya varsayılanlı)
- Deploy: uygulama hem ESKİ hem YENİ'ye yazar
- Mevcut veriyi backfill et
Faz 2: MIGRATE
- Deploy: uygulama YENİ'den okur, her İKİSİNE yazar
- Veri tutarlılığını doğrula
Faz 3: CONTRACT
- Deploy: uygulama sadece YENİ'yi kullanır
- Eski sütun/tabloyu ayrı migration'da kaldır
Gün 1: Migration new_status sütunu ekler (nullable)
Gün 1: App v2 deploy et — hem status hem new_status'a yaz
Gün 2: Mevcut satırlar için backfill migration'ı çalıştır
Gün 3: App v3 deploy et — sadece new_status'tan okur
Gün 7: Migration eski status sütununu kaldırır
| Anti-Kalıp | Neden Başarısız Olur | Daha İyi Yaklaşım |
|---|---|---|
| Üretimde manuel SQL | Denetim izi yok, tekrarlanamaz | Her zaman migration dosyaları kullan |
| Deploy edilmiş migration'ları düzenleme | Ortamlar arası sapma yaratır | Bunun yerine yeni migration oluştur |
| Varsayılansız NOT NULL | Tabloyu kilitler, tüm satırları yeniden yazar | Nullable ekle, backfill et, sonra kısıt ekle |
| Büyük tabloda inline indeks | Build sırasında yazmaları engeller | CREATE INDEX CONCURRENTLY |
| Tek migration'da şema + veri | Rollback zor, uzun transaction'lar | Ayrı migration'lar |
| Kodu kaldırmadan önce sütun kaldırma | Eksik sütunda uygulama hataları | Önce kodu kaldır, sonra sütunu sonraki deploy'da kaldır |
🧭 Что это
Database Migration Patterns — это набор проверенных практик и готовых сценариев для безопасного управления изменениями схемы и данных в реляционных базах данных (PostgreSQL, MySQL) и популярных ORM (Prisma, Drizzle, Django, TypeORM, golang-migrate). Skill помогает избежать простоев, блокировок таблиц и потери данных при деплое миграций в production-среде.
⚙️ Как работает
🔐 Базовые принципы
- Каждое изменение — миграция. Никогда не вносите правки в production-базу вручную.
- Миграции только вперёд. Откат — это новая миграция, а не запуск
DOWN(за исключением явных rollback-сценариев). - DDL и DML разделены: никогда не смешивайте изменение схемы (ALTER TABLE) и модификацию данных (UPDATE) в одной миграции.
- Тестируйте на production-размере данных. Миграция, работающая на 100 строк, может заблокировать таблицу на 10M.
- Миграции неизменяемы после применения в production. Не редактируйте уже выполненную миграцию.
🛡️ Чеклист безопасности перед выполнением
- ✅ Миграция содержит
UPиDOWN(или явно помечена как неоткатываемая). - ✅ Нет полных блокировок таблиц на больших объёмах (
CREATE INDEXбезCONCURRENTLY,ALTER TABLE ... SET NOT NULLбез предварительного backfill). - ✅ Новые столбцы —
nullableили сDEFAULT. Никогда не добавляйтеNOT NULLбез значения по умолчанию. - ✅ Индексы создаются через
CREATE INDEX CONCURRENTLYдля существующих таблиц. - ✅ Data backfill вынесен в отдельную миграцию после изменения схемы.
- ✅ Протестировано на копии production-данных.
- ✅ План отката задокументирован.
📘 PostgreSQL: безопасные паттерны
➕ Добавление столбца
Хорошо:
ALTER TABLE users ADD COLUMN avatar_url TEXT; -- nullable, без блокировки
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true; -- Postgres 11+, мгновенно
Плохо:
ALTER TABLE users ADD COLUMN role TEXT NOT NULL; -- блокирует таблицу и перезаписывает все строки
🧵 Добавление индекса без блокировки
Плохо (блокирует запись):
CREATE INDEX idx_users_email ON users (email);
Хорошо (разрешена параллельная запись):
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
-- НЕ выполняется внутри транзакционного блока!
🔁 Переименование столбца (expand-contract)
Никогда не переименовывайте столбец напрямую в production. Используйте многошаговый паттерн:
- Expand — добавить новый столбец (
display_name). - Backfill — перенести данные отдельной миграцией:
UPDATE users SET display_name = username WHERE display_name IS NULL;. - Deploy — обновить код приложения на чтение/запись обоих столбцов.
- Contract — удалить старый столбец после того, как приложение перестанет его использовать.
❌ Удаление столбца
Удалять можно только после того, как все ссылки в коде приложения убраны и деплой выполнен. В следующей миграции:
ALTER TABLE orders DROP COLUMN legacy_status;
Для Django используйте SeparateDatabaseAndState, чтобы сначала изменить модель, а столбец удалить позже.
📦 Массовые обновления (data migration)
Плохо — одна транзакция на все строки (блокирует таблицу):
UPDATE users SET normalized_email = LOWER(email);
Хорошо — пакетная обработка с SKIP LOCKED:
DO $$
DECLARE batch_size INT := 10000; rows_updated INT;
BEGIN
LOOP
UPDATE users SET normalized_email = LOWER(email)
WHERE id IN (SELECT id FROM users WHERE normalized_email IS NULL LIMIT batch_size FOR UPDATE SKIP LOCKED);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
EXIT WHEN rows_updated = 0;
COMMIT;
END LOOP;
END $$;
🛠️ Инструменты и их команды
Prisma (TypeScript/Node.js)
# Создать миграцию из схемы
npx prisma migrate dev --name add_user_avatar
# Применить в production
npx prisma migrate deploy
# Сбросить БД (только dev)
npx prisma migrate reset
# Сгенерировать клиент после изменений
npx prisma generate
Для операций, неподдерживаемых Prisma (например, CONCURRENTLY), создаётся пустая миграция и SQL редактируется вручную:
npx prisma migrate dev --create-only --name add_email_index
Drizzle (TypeScript/Node.js)
npx drizzle-kit generate # из схемы
npx drizzle-kit migrate # применить
npx drizzle-kit push # только dev, без файла миграции
Django (Python)
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrations
python manage.py makemigrations --empty app_name -n description # для кастомного SQL
Пример data migration внутри Django:
def backfill_display_names(apps, schema_editor):
User = apps.get_model("accounts", "User")
batch_size = 5000
while users.exists():
batch = list(users[:batch_size])
for user in batch:
user.display_name = user.username
User.objects.bulk_update(batch, ["display_name"], batch_size=batch_size)
golang-migrate (Go)
migrate create -ext sql -dir migrations -seq add_user_avatar
migrate -path migrations -database "$DATABASE_URL" up
migrate -path migrations -database "$DATABASE_URL" down 1
migrate -path migrations -database "$DATABASE_URL" force VERSION # исправить dirty-статус
Пример файла миграции:
-- 000003_add_user_avatar.up.sql
ALTER TABLE users ADD COLUMN avatar_url TEXT;
CREATE INDEX CONCURRENTLY idx_users_avatar ON users (avatar_url) WHERE avatar_url IS NOT NULL;
🚀 Zero-downtime migration (expand-contract)
Стратегия для критических изменений, не допускающих простоя:
- Фаза 1 (EXPAND): добавить новый столбец/таблицу (nullable или с default). Деплой приложения, которое пишет и в старый, и в новый атрибут.
- Фаза 2 (MIGRATE): запустить backfill существующих данных. Приложение читает из нового, пишет в оба.
- Фаза 3 (CONTRACT): убрать старый столбец/таблицу отдельной миграцией, когда приложение полностью перешло на новое поле.
Пример временного графика:
- День 1: миграция
new_status(nullable). Деплой app v2 (пишет в оба поля). - День 2: backfill старых строк.
- День 3: деплой app v3 (читает только
new_status). - День 7: миграция удаляет старый
status.
✅ Когда использовать
- При каждом изменении схемы БД (добавление/удаление таблиц, столбцов, индексов).
- Для выполнения data migration (backfill, трансформация данных).
- При планировании изменений без остановки сервиса.
- При настройке инструментов миграции в новом проекте.
⚠️ Важно знать: анти-паттерны
| Анти-паттерн | Почему проваливается | Правильный подход |
|---|---|---|
| Вручную выполнять SQL в production | Нет аудита, нельзя повторить | Всегда используйте файлы миграций |
| Редактировать уже применённую миграцию | Расхождение между окружениями | Создайте новую миграцию |
Добавлять NOT NULL без default |
Блокирует таблицу и переписывает все строки | Добавьте nullable, сделайте backfill, затем добавьте NOT NULL |
| Инлайн-индекс на большой таблице | Блокирует запись на всё время создания | CREATE INDEX CONCURRENTLY |
| Смешивать DDL и DML в одной миграции | Сложно откатить, долгая транзакция | Разделите на отдельные миграции |
| Удалять столбец до того, как код перестал его использовать | Ошибки приложения | Сначала удалите все ссылки в коде и сделайте деплой, затем удалите столбец |
Всегда помните: миграция — это код. Она должна быть версионирована, рецензируема и тестируема.
Зачем разделять миграции схемы и данных?
Разделение DDL и DML предотвращает блокировку таблиц длительными транзакциями, а также делает процедуры отката значительно безопаснее и проще в управлении.
Как избежать блокировки таблиц во время миграций?
Используйте параллельное создание индексов и шаблон expand-contract: сначала добавляйте nullable-колонки, переносите данные, а ограничения применяйте в следующем развертывании.
Что такое шаблон expand-contract?
Это многоэтапная стратегия: сначала расширяете схему (добавляете новые поля), переносите данные, а затем сжимаете (удаляете старые поля) после того, как приложение перестанет их использовать.
Какие ORM поддерживаются этим навыком?
Он предоставляет конкретные рекомендации и примеры кода для Prisma, Drizzle, Django, TypeORM и golang-migrate, а также шаблоны для чистого PostgreSQL и MySQL.
Синхронизируйте навыки в Claude Cowork, Claude Code, Codex и другие.
Установка одной командой.
npx skillfish add affaan-m/everything-claude-code database-migrationsИсточник: https://mcpmarket.com/tools/skills/database-migration-patterns-10
Комментарии
Комментариев пока нет. Будьте первым.