目次
- データベースマイグレーションの基本
- なぜマイグレーションが必要なのか
- マイグレーションの基本的な流れ
- マイグレーションツールの比較と使い方
- Prisma Migrate(TypeScript/JavaScript)
- Drizzle Kit(TypeScript)
- golang-migrate(Go / 汎用CLIツール)
- 安全なマイグレーションの原則
- 原則1:前方互換性を保つ
- 原則2:小さな変更を頻繁に
- 原則3:必ずロールバック手順を用意する
- ゼロダウンタイムマイグレーションの手法
- カラムの追加(安全)
- NOT NULL制約の安全な追加
- インデックスの安全な作成
- テーブル名・カラム名の変更(Expand/Contract パターン)
- 大量データの安全なマイグレーション
- バッチ処理でのデータ移行
- PostgreSQLのバッチUPDATE
- マイグレーションのベストプラクティス
- CI/CDパイプラインでのマイグレーション
- マイグレーション前のチェックリスト
- マイグレーションファイルの命名規則
- まとめ
「本番環境のテーブルにカラムを追加したいけれど、サービスを止めずにできるのか?」「マイグレーションを実行したら戻せなくなった」「チーム開発でマイグレーションの競合が起きる」――データベースのスキーマ変更は、アプリケーション開発で避けて通れない作業ですが、失敗すると深刻な障害につながります。
本記事では、データベースマイグレーションの基本概念から、安全に実行するための戦略、ゼロダウンタイムマイグレーションの手法、主要なツールの使い方まで、実践的なガイドとして解説します。
データベースマイグレーションの基本
データベースマイグレーションとは、データベースのスキーマ(テーブル構造、カラム定義、インデックスなど)をバージョン管理しながら段階的に変更していくプロセスです。
なぜマイグレーションが必要なのか
アプリケーションの機能追加や改善に伴い、データベースのスキーマも変化します。手動でSQL文を実行する方法では、以下の問題が発生します。
- 再現性がない:誰がいつ何を変更したか追跡できない
- 環境差異:開発環境とステージング環境、本番環境でスキーマが異なる
- チーム開発:複数の開発者が同時にスキーマを変更する際に競合が起きる
- ロールバック困難:問題が起きた際に元の状態に戻せない
マイグレーションツールを使えば、スキーマの変更をコードとしてバージョン管理し、どの環境でも同じ手順で適用・ロールバックできます。
マイグレーションの基本的な流れ
# 一般的なマイグレーションの流れ
1. マイグレーションファイルを作成(up/down の SQL を記述)
2. ローカル環境で適用してテスト
3. コードレビュー(PR に含める)
4. ステージング環境で適用してテスト
5. 本番環境で適用
6. 問題があればロールバック(down を実行)
マイグレーションツールの比較と使い方
主要なマイグレーションツールを紹介します。プロジェクトの技術スタックに合わせて選択しましょう。
Prisma Migrate(TypeScript/JavaScript)
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
# マイグレーションの作成と適用
npx prisma migrate dev --name add_user_and_post_tables
# 本番環境への適用
npx prisma migrate deploy
# マイグレーション状態の確認
npx prisma migrate status
# スキーマのリセット(開発環境のみ!)
npx prisma migrate reset
Drizzle Kit(TypeScript)
// src/db/schema.ts
import { pgTable, serial, varchar, text, boolean, timestamp, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).unique().notNull(),
name: varchar('name', { length: 100 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
published: boolean('published').default(false),
authorId: integer('author_id').references(() => users.id).notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
# マイグレーションファイルの生成
npx drizzle-kit generate
# マイグレーションの適用
npx drizzle-kit migrate
# スキーマの差分確認
npx drizzle-kit check
golang-migrate(Go / 汎用CLIツール)
言語に依存しない汎用的なマイグレーションツールで、生SQLファイルを管理します。
# インストール
brew install golang-migrate
# マイグレーションファイルの作成
migrate create -ext sql -dir db/migrations -seq create_users_table
-- db/migrations/000001_create_users_table.up.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
-- db/migrations/000001_create_users_table.down.sql
DROP TABLE IF EXISTS users;
# マイグレーションの適用
migrate -path db/migrations -database "postgresql://user:pass@localhost:5432/mydb?sslmode=disable" up
# 1つ戻す
migrate -path db/migrations -database "..." down 1
# 特定バージョンまで適用
migrate -path db/migrations -database "..." goto 3
# 現在のバージョン確認
migrate -path db/migrations -database "..." version
安全なマイグレーションの原則
本番環境でマイグレーションを安全に実行するために、守るべき原則があります。
原則1:前方互換性を保つ
マイグレーションとアプリケーションのデプロイは同時に行われるとは限りません。古いコードと新しいスキーマ、新しいコードと古いスキーマ、どちらでも動作するように設計しましょう。
-- 悪い例:カラム名の変更を1ステップで行う
ALTER TABLE users RENAME COLUMN name TO display_name;
-- → 古いコードが「name」カラムを参照してエラーになる
-- 良い例:3ステップに分ける
-- Step 1: 新カラムを追加
ALTER TABLE users ADD COLUMN display_name VARCHAR(100);
UPDATE users SET display_name = name;
-- Step 2: アプリケーションを更新(両方のカラムに書き込む)
-- ... デプロイ ...
-- Step 3: 古いカラムを削除(すべてのサーバーが新コードに切り替わった後)
ALTER TABLE users DROP COLUMN name;
原則2:小さな変更を頻繁に
大きなマイグレーションは失敗リスクが高く、ロールバックも困難です。変更を小さく分割しましょう。
-- 悪い例:1つのマイグレーションに全部詰め込む
-- migration_20260327_big_refactor.sql
CREATE TABLE new_users (...);
INSERT INTO new_users SELECT ... FROM old_users;
DROP TABLE old_users;
ALTER TABLE new_users RENAME TO users;
CREATE TABLE orders (...);
CREATE TABLE products (...);
-- ... 数百行のSQL
-- 良い例:小さなマイグレーションに分割
-- 001_create_new_users.sql
-- 002_migrate_user_data.sql
-- 003_create_orders.sql
-- 004_create_products.sql
-- 005_drop_old_users.sql
原則3:必ずロールバック手順を用意する
-- up.sql
ALTER TABLE products ADD COLUMN sku VARCHAR(50);
CREATE UNIQUE INDEX idx_products_sku ON products(sku);
-- down.sql(ロールバック)
DROP INDEX IF EXISTS idx_products_sku;
ALTER TABLE products DROP COLUMN IF EXISTS sku;
ただし、データの削除を伴うマイグレーションは完全なロールバックが不可能です。カラムやテーブルの削除は慎重に行い、事前にバックアップを取得しましょう。
ゼロダウンタイムマイグレーションの手法
サービスを停止せずにスキーマ変更を行う手法を解説します。
カラムの追加(安全)
-- PostgreSQLでのカラム追加はロックが軽い(AccessExclusiveLockは一瞬)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- デフォルト値を持つカラムの追加
-- PostgreSQL 11以降は即座に完了(テーブルの書き換えが発生しない)
ALTER TABLE users ADD COLUMN is_verified BOOLEAN DEFAULT false;
NOT NULL制約の安全な追加
-- 悪い例:直接NOT NULLを追加(テーブル全体のスキャンが発生)
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
-- 良い例:段階的にNOT NULLを追加
-- Step 1: CHECK制約をNOT VALIDで追加(即座に完了、既存行はチェックしない)
ALTER TABLE users ADD CONSTRAINT users_phone_not_null
CHECK (phone IS NOT NULL) NOT VALID;
-- Step 2: 既存のNULLデータを埋める
UPDATE users SET phone = '未設定' WHERE phone IS NULL;
-- Step 3: 制約をVALIDATEする(Share Update Exclusive Lock、読み書きは可能)
ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null;
-- Step 4: NOT NULL制約を追加しCHECK制約を削除
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_phone_not_null;
インデックスの安全な作成
-- 悪い例:通常のCREATE INDEX(テーブルへの書き込みがブロックされる)
CREATE INDEX idx_posts_title ON posts(title);
-- 良い例:CONCURRENTLYオプション(書き込みをブロックしない)
CREATE INDEX CONCURRENTLY idx_posts_title ON posts(title);
-- CONCURRENTLYの注意点
-- ・トランザクション内では使用できない
-- ・失敗した場合はINVALIDなインデックスが残る → 手動で削除が必要
-- ・通常のCREATE INDEXより時間がかかる
テーブル名・カラム名の変更(Expand/Contract パターン)
-- Expand/Contractパターンでカラム名を安全に変更する
-- Phase 1: Expand(拡張)
-- 新しいカラムを追加し、トリガーで同期
ALTER TABLE users ADD COLUMN display_name VARCHAR(100);
CREATE OR REPLACE FUNCTION sync_user_name()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
NEW.display_name = COALESCE(NEW.display_name, NEW.name);
NEW.name = COALESCE(NEW.name, NEW.display_name);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_user_name
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_user_name();
-- 既存データの同期
UPDATE users SET display_name = name WHERE display_name IS NULL;
-- Phase 2: Migrate(移行)
-- アプリケーションを新しいカラム名に切り替え
-- Phase 3: Contract(収縮)
-- 古いカラムとトリガーを削除
DROP TRIGGER trg_sync_user_name ON users;
DROP FUNCTION sync_user_name;
ALTER TABLE users DROP COLUMN name;
大量データの安全なマイグレーション
数百万行以上のテーブルでデータ変換を行う場合、一括処理はロックやディスクI/Oの問題を引き起こします。
バッチ処理でのデータ移行
// Node.jsでのバッチ処理例
async function migrateData() {
const BATCH_SIZE = 1000;
let lastId = 0;
let processedCount = 0;
while (true) {
const rows = await db.query(`
SELECT id, name
FROM users
WHERE id > $1 AND display_name IS NULL
ORDER BY id
LIMIT $2
`, [lastId, BATCH_SIZE]);
if (rows.length === 0) break;
// バッチ単位でUPDATE
for (const row of rows) {
await db.query(`
UPDATE users
SET display_name = $1
WHERE id = $2
`, [row.name, row.id]);
}
lastId = rows[rows.length - 1].id;
processedCount += rows.length;
console.log(`Processed ${processedCount} rows (last id: ${lastId})`);
// 負荷を抑えるため少し待機
await new Promise(r => setTimeout(r, 100));
}
console.log(`Migration complete: ${processedCount} rows processed`);
}
PostgreSQLのバッチUPDATE
-- CTEを使ったバッチUPDATE(1000件ずつ処理)
WITH batch AS (
SELECT id
FROM users
WHERE display_name IS NULL
ORDER BY id
LIMIT 1000
FOR UPDATE SKIP LOCKED -- ロック競合を回避
)
UPDATE users
SET display_name = name
WHERE id IN (SELECT id FROM batch);
-- このクエリを影響行数が0になるまで繰り返す
マイグレーションのベストプラクティス
CI/CDパイプラインでのマイグレーション
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run migrations on staging
run: |
npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
- name: Run integration tests
run: npm test
- name: Run migrations on production
run: |
npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}
- name: Deploy application
run: |
# アプリケーションのデプロイ
マイグレーション前のチェックリスト
- バックアップの取得:マイグレーション前に必ずバックアップを取る
- ステージング環境でのテスト:本番と同じデータ量でテスト
- ロールバック手順の確認:down マイグレーションが正しく動作するか確認
- 実行時間の見積もり:大きなテーブルの場合、ALTERの所要時間を事前に把握
- ロックの確認:どのレベルのロックが取得されるか理解しておく
- アプリケーションの互換性:古いバージョンのコードでも動作するか確認
-- バックアップの取得(pg_dump)
pg_dump -h localhost -U postgres -d mydb -F c -f backup_20260327.dump
-- リストア(問題が発生した場合)
pg_restore -h localhost -U postgres -d mydb backup_20260327.dump
マイグレーションファイルの命名規則
# タイムスタンプベース(推奨:チーム開発での競合が少ない)
20260327120000_create_users_table.sql
20260327120100_add_email_to_users.sql
20260327120200_create_posts_table.sql
# シーケンス番号ベース
001_create_users_table.sql
002_add_email_to_users.sql
003_create_posts_table.sql
まとめ
データベースマイグレーションを安全に行うためのポイントを整理します。
- マイグレーションツールを使う:手動SQL実行ではなく、ツールでバージョン管理する
- 前方互換性を保つ:古いコードと新しいスキーマが共存できるように設計する
- 小さく分割する:大きなマイグレーションを小さなステップに分ける
- ロールバック手順を用意する:down マイグレーションを必ず書く
- ゼロダウンタイム:CONCURRENTLY、NOT VALID、Expand/Contractパターンを活用
- 大量データは段階的に:バッチ処理で負荷を分散する
- CI/CDに組み込む:自動化してヒューマンエラーを防ぐ
データベースマイグレーションは「やり直しが効かない」作業だからこそ、慎重な計画と事前テストが重要です。本記事で紹介した原則とテクニックを活用し、安全なスキーマ変更を実践してください。
先行モニター企業 募集中|先着5社・2026年11月末まで 生成AIの利用費、「誰が・何のために・いくら」まで見えていますか?
株式会社カイダンと共同開発したAI利用ガバナンスSaaS「ManageAI」は、OpenAI・Azure OpenAI・Claude・AWS Bedrock・Google Vertex AIの費用と利用状況を、部署・利用者・プロジェクト別に可視化します。導入は読み取り専用キーの登録だけ。初期費用0円・月額5万円(税別)で先行モニター企業を募集しています。
ニュースリリースを読む関連記事
Related / 9 articles
Notes & Insights
- プログラミング
DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用
DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。
- プログラミング
WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド
WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。
- プログラミング
セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説
CSP・HSTS・X-Frame-Options・X-Content-Type-Optionsなど主要セキュリティヘッダーが防ぐ攻撃と、CSPのReport-Onlyからの段階導入、nonce/hash、HSTS preloadの不可逆リスク、Nginx・Next.js・Astroでの設定例、確認方法を解説します。
- プログラミング
クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計
クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。
- プログラミング
秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除
APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。
- プログラミング
開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御
開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。
- プログラミング
ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方
NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。
- プログラミング
ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方
ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。
- プログラミング
localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決
localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。
先行モニター企業 募集中
生成AIのコストを「部署別」に、請求書より先に。
先着5社限定・2026年11月末まで。初期費用0円・月額5万円(税別)で、正式版と同等の機能に加え、開発者と伴走者による導入支援を追加費用なしで提供します。プロンプト原文は一切取得しません。
株式会社カイダン × Harmonic Society株式会社の共同事業|読み取り専用キーの登録だけで導入できます