はじめに

データベーススキーマは、アプリケーションの成長とともに変化し続けます。新機能の追加によるカラムの追加、パフォーマンス改善のためのデータ型変更、不要になったカラムの削除など、本番環境で稼働しているデータベースに対して安全にスキーマ変更を行う能力は、現代のソフトウェア開発において不可欠なスキルです。

本記事では、PostgreSQLにおけるALTER TABLEの基本操作から、マイグレーションツール(Flyway/Liquibase/Prisma Migrate)を活用した体系的なスキーマ管理、そして本番環境でのスキーマ変更における注意点まで、実践的なサンプルコードとともに解説します。

この記事を読むことで、以下のことができるようになります。

  • ALTER TABLEを使ってカラムの追加・削除・型変更ができる
  • 制約(NOT NULL/DEFAULT/CHECK等)を安全に変更できる
  • マイグレーションツールを使ってスキーマ変更を体系的に管理できる
  • 本番環境でのスキーマ変更におけるリスクと対策を理解できる
  • ダウンタイムを最小化するスキーマ変更戦略を実践できる

前提条件

  • PostgreSQL 14以降がインストールされていること
  • psqlまたはGUIツール(pgAdmin、DBeaver等)でデータベースに接続できること
  • CREATE TABLE文とテーブル設計の基本を理解していること
  • トランザクション(BEGIN/COMMIT/ROLLBACK)の概念を理解していること

サンプルデータの準備

本記事で使用するサンプルテーブルを準備します。以下のSQLを実行してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- ユーザーテーブルの作成
CREATE TABLE users (
    user_id      SERIAL PRIMARY KEY,
    email        VARCHAR(255) NOT NULL UNIQUE,
    name         VARCHAR(100) NOT NULL,
    created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 商品テーブルの作成
CREATE TABLE products (
    product_id   SERIAL PRIMARY KEY,
    name         VARCHAR(100) NOT NULL,
    price        INTEGER NOT NULL,
    stock        INTEGER DEFAULT 0
);

-- サンプルデータの挿入
INSERT INTO users (email, name) VALUES
('tanaka@example.com', '田中太郎'),
('suzuki@example.com', '鈴木花子'),
('yamada@example.com', '山田一郎');

INSERT INTO products (name, price, stock) VALUES
('ノートPC', 120000, 25),
('マウス', 3500, 150),
('キーボード', 8000, 80);

ALTER TABLEの基本構文

ALTER TABLEは、既存のテーブル構造を変更するためのDDL(Data Definition Language)文です。PostgreSQLでは、カラムの追加・削除・変更、制約の追加・削除、テーブル名やカラム名の変更など、多彩な操作が可能です。

基本的な構文パターン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- カラムの追加
ALTER TABLE テーブル名 ADD COLUMN カラム名 データ型 [制約];

-- カラムの削除
ALTER TABLE テーブル名 DROP COLUMN カラム名;

-- カラムのデータ型変更
ALTER TABLE テーブル名 ALTER COLUMN カラム名 TYPE 新しいデータ型;

-- カラム名の変更
ALTER TABLE テーブル名 RENAME COLUMN 旧カラム名 TO 新カラム名;

-- テーブル名の変更
ALTER TABLE 旧テーブル名 RENAME TO 新テーブル名;

-- 制約の追加
ALTER TABLE テーブル名 ADD CONSTRAINT 制約名 制約定義;

-- 制約の削除
ALTER TABLE テーブル名 DROP CONSTRAINT 制約名;

カラムの追加

基本的なカラム追加

既存のテーブルに新しいカラムを追加する最も基本的な例です。

1
2
-- usersテーブルに電話番号カラムを追加
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

このコマンドを実行すると、既存の全行に対してNULL値が設定されます。

1
2
-- 追加結果の確認
SELECT * FROM users;
user_id email name created_at phone
1 tanaka@example.com 田中太郎 2026-01-03 10:00:00 NULL
2 suzuki@example.com 鈴木花子 2026-01-03 10:00:00 NULL
3 yamada@example.com 山田一郎 2026-01-03 10:00:00 NULL

デフォルト値付きのカラム追加

カラム追加時にデフォルト値を指定すると、既存行にはそのデフォルト値が設定されます。

1
2
3
4
5
-- 有効フラグカラムをデフォルト値trueで追加
ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT TRUE;

-- 結果確認
SELECT user_id, name, is_active FROM users;
user_id name is_active
1 田中太郎 true
2 鈴木花子 true
3 山田一郎 true

NOT NULL制約付きカラムの追加

NOT NULL制約付きのカラムを追加する場合、既存行に対してNULLを設定できないため、DEFAULTの指定が必須です。

1
2
3
4
5
6
-- NG: デフォルト値なしでNOT NULLカラムを追加しようとするとエラー
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL;
-- ERROR: column "status" of relation "users" contains null values

-- OK: デフォルト値を指定してNOT NULLカラムを追加
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active';

複数カラムの同時追加

PostgreSQLでは、複数のALTER操作を1つのコマンドで実行できます。これにより、テーブルロックの取得回数を減らし、パフォーマンスを向上させることができます。

1
2
3
4
-- 複数カラムを同時に追加
ALTER TABLE users 
    ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    ADD COLUMN last_login_at TIMESTAMP;

カラムの削除

基本的なカラム削除

不要になったカラムを削除します。

1
2
-- phoneカラムを削除
ALTER TABLE users DROP COLUMN phone;

IF EXISTSオプション

カラムが存在しない場合にエラーを発生させたくない場合は、IF EXISTSオプションを使用します。

1
2
3
-- カラムが存在する場合のみ削除
ALTER TABLE users DROP COLUMN IF EXISTS phone;
-- NOTICE:  column "phone" of relation "users" does not exist, skipping

CASCADE オプション

削除対象のカラムがビューや他のオブジェクトから参照されている場合、CASCADEオプションを指定することで依存オブジェクトごと削除できます。

1
2
-- 依存オブジェクトがあるカラムを削除
ALTER TABLE users DROP COLUMN status CASCADE;

ただし、CASCADEは意図しないオブジェクトも削除する可能性があるため、本番環境では慎重に使用してください。

データ型の変更

互換性のある型変更

暗黙的な型変換が可能な場合、単純なTYPE指定で変更できます。

1
2
3
4
5
-- VARCHARの長さを拡張
ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(200);

-- INTEGERからBIGINTへの変更
ALTER TABLE products ALTER COLUMN price TYPE BIGINT;

USING句による型変換

暗黙的な変換ができない場合は、USING句で変換式を指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 文字列型の価格カラムを整数型に変換
-- 事前準備:テスト用テーブル
CREATE TABLE test_products (
    id SERIAL PRIMARY KEY,
    price_text VARCHAR(20)
);
INSERT INTO test_products (price_text) VALUES ('1000'), ('2500'), ('500');

-- USING句で変換式を指定
ALTER TABLE test_products 
    ALTER COLUMN price_text TYPE INTEGER 
    USING price_text::INTEGER;

型変更時の注意点

データ型の変更は、テーブル全体の書き換えが発生する場合があります。特に大規模なテーブルでは、以下の点に注意が必要です。

  • ロック: ALTER TABLE中はテーブルに排他ロック(ACCESS EXCLUSIVE)がかかります
  • 変換エラー: 既存データが新しい型に変換できない場合はエラーになります
  • ディスク容量: テーブル書き換え中は一時的に2倍のディスク容量が必要です
1
2
3
4
-- 安全な型変更の前に、変換可能かどうかを確認
SELECT price_text, price_text::INTEGER 
FROM test_products 
WHERE price_text !~ '^\d+$';

制約の変更

NOT NULL制約の追加と削除

1
2
3
4
5
-- NOT NULL制約を追加
ALTER TABLE users ALTER COLUMN name SET NOT NULL;

-- NOT NULL制約を削除
ALTER TABLE users ALTER COLUMN name DROP NOT NULL;

NOT NULL制約を追加する際、既存行にNULL値が含まれているとエラーになります。

1
2
3
4
5
6
7
-- NULL値を含むカラムにNOT NULLを追加しようとするとエラー
ALTER TABLE users ALTER COLUMN last_login_at SET NOT NULL;
-- ERROR: column "last_login_at" of relation "users" contains null values

-- 先にNULL値を更新してから制約を追加
UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE last_login_at IS NULL;
ALTER TABLE users ALTER COLUMN last_login_at SET NOT NULL;

DEFAULT値の設定と削除

1
2
3
4
5
-- デフォルト値を設定
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';

-- デフォルト値を削除
ALTER TABLE users ALTER COLUMN status DROP DEFAULT;

CHECK制約の追加

1
2
3
4
5
6
7
8
9
-- 価格が0以上であることを保証するCHECK制約を追加
ALTER TABLE products 
    ADD CONSTRAINT products_price_positive 
    CHECK (price >= 0);

-- 複合CHECK制約
ALTER TABLE products 
    ADD CONSTRAINT products_stock_valid 
    CHECK (stock >= 0 AND stock <= 10000);

UNIQUE制約の追加

1
2
3
4
-- 商品名のユニーク制約を追加
ALTER TABLE products 
    ADD CONSTRAINT products_name_unique 
    UNIQUE (name);

外部キー制約の追加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- 注文テーブルの作成と外部キー制約の追加
CREATE TABLE orders (
    order_id    SERIAL PRIMARY KEY,
    user_id     INTEGER,
    product_id  INTEGER,
    quantity    INTEGER NOT NULL,
    order_date  DATE DEFAULT CURRENT_DATE
);

-- 外部キー制約を後から追加
ALTER TABLE orders 
    ADD CONSTRAINT orders_user_fk 
    FOREIGN KEY (user_id) REFERENCES users(user_id);

ALTER TABLE orders 
    ADD CONSTRAINT orders_product_fk 
    FOREIGN KEY (product_id) REFERENCES products(product_id);

制約の削除

1
2
3
4
5
6
7
-- 制約名を指定して削除
ALTER TABLE products DROP CONSTRAINT products_price_positive;

-- 制約名が不明な場合は、システムカタログから確認
SELECT conname, contype, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'products'::regclass;

カラム名とテーブル名の変更

カラム名の変更

1
2
3
4
5
-- カラム名を変更
ALTER TABLE users RENAME COLUMN name TO full_name;

-- 確認
\d users

テーブル名の変更

1
2
3
4
5
-- テーブル名を変更
ALTER TABLE products RENAME TO items;

-- 元に戻す
ALTER TABLE items RENAME TO products;

カラム名やテーブル名を変更すると、それを参照しているビュー、関数、アプリケーションコードも更新が必要になります。変更前に影響範囲を十分に調査してください。

安全なスキーマ変更のパターン

本番環境でスキーマ変更を行う際は、ダウンタイムを最小化し、ロールバック可能な状態を維持することが重要です。以下に、安全なスキーマ変更のパターンを紹介します。

NOT VALID制約と段階的検証

大規模テーブルに制約を追加する場合、NOT VALIDオプションを使用することで、既存データの検証をスキップし、ロック時間を短縮できます。

1
2
3
4
5
6
7
8
-- Step 1: NOT VALIDで制約を追加(既存データの検証をスキップ)
ALTER TABLE products 
    ADD CONSTRAINT products_price_check 
    CHECK (price > 0) 
    NOT VALID;

-- Step 2: バックグラウンドで制約を検証(SHARE UPDATE EXCLUSIVEロック)
ALTER TABLE products VALIDATE CONSTRAINT products_price_check;

NOT VALIDで追加した制約は、新規INSERT/UPDATEには適用されますが、既存データは検証されません。VALIDATE CONSTRAINTを実行することで、既存データの検証を行い、制約を完全に有効化します。

CONCURRENTLYインデックス作成

通常のCREATE INDEXは排他ロックを取得しますが、CONCURRENTLYオプションを使用すると、テーブルへの読み書きをブロックせずにインデックスを作成できます。

1
2
3
4
5
-- 通常のインデックス作成(テーブルがロックされる)
CREATE INDEX products_name_idx ON products(name);

-- CONCURRENTLY オプション(読み書きをブロックしない)
CREATE INDEX CONCURRENTLY products_price_idx ON products(price);

CONCURRENTLYオプションの注意点は以下の通りです。

  • トランザクション内では使用できない
  • 作成に通常より時間がかかる
  • 失敗した場合、無効なインデックスが残る可能性がある
1
2
3
4
5
6
7
8
-- 無効なインデックスの確認と削除
SELECT indexname, indexdef 
FROM pg_indexes 
WHERE tablename = 'products' AND indexdef IS NULL;

-- 無効なインデックスを削除して再作成
DROP INDEX CONCURRENTLY products_price_idx;
CREATE INDEX CONCURRENTLY products_price_idx ON products(price);

段階的なスキーマ変更

大きなスキーマ変更を一度に行うのではなく、複数の小さな変更に分割して段階的に適用します。

flowchart LR
    A[Step 1<br/>新カラム追加<br/>NULL許可] --> B[Step 2<br/>アプリで<br/>両カラム対応]
    B --> C[Step 3<br/>データ移行]
    C --> D[Step 4<br/>NOT NULL追加]
    D --> E[Step 5<br/>旧カラム削除]

具体的な例として、emailカラムをemailsテーブルに分離する場合を考えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- Step 1: 新しいテーブルを作成
CREATE TABLE user_emails (
    email_id    SERIAL PRIMARY KEY,
    user_id     INTEGER NOT NULL REFERENCES users(user_id),
    email       VARCHAR(255) NOT NULL,
    is_primary  BOOLEAN DEFAULT TRUE,
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Step 2: データを移行
INSERT INTO user_emails (user_id, email, is_primary)
SELECT user_id, email, TRUE FROM users;

-- Step 3: アプリケーションを新テーブル対応に更新

-- Step 4: 旧カラムを削除
ALTER TABLE users DROP COLUMN email;

マイグレーションツールの活用

手動でのスキーマ変更は、複数環境間での整合性維持やロールバックが困難です。マイグレーションツールを使用することで、スキーマ変更をバージョン管理し、再現可能な形で適用できます。

マイグレーションツールの比較

ツール 特徴 対象言語/フレームワーク
Flyway SQLベース、シンプル Java/Spring Boot、汎用
Liquibase XML/YAML/JSON/SQL対応、ロールバック機能 Java/Spring Boot、汎用
Prisma Migrate TypeScript統合、型安全 Node.js/TypeScript
Alembic SQLAlchemy統合 Python
golang-migrate 軽量、CLI中心 Go

Flywayによるマイグレーション管理

Flyway は、SQLファイルをバージョン管理してデータベースに順次適用するマイグレーションツールです。ファイル名の規約に従うだけで、バージョン管理と適用順序の制御が自動化されます。

Flywayのセットアップ

1
2
3
4
5
6
7
8
# Dockerでの起動例
docker run --rm \
  -v $(pwd)/migrations:/flyway/sql \
  flyway/flyway \
  -url=jdbc:postgresql://host.docker.internal:5432/mydb \
  -user=postgres \
  -password=postgres \
  migrate

マイグレーションファイルの命名規則

V{バージョン番号}__{説明}.sql

例:

  • V1__create_users_table.sql
  • V2__add_phone_column.sql
  • V3__create_orders_table.sql

マイグレーションファイルの例

V1__create_users_table.sql:

1
2
3
4
5
6
CREATE TABLE users (
    user_id      SERIAL PRIMARY KEY,
    email        VARCHAR(255) NOT NULL UNIQUE,
    name         VARCHAR(100) NOT NULL,
    created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

V2__add_phone_column.sql:

1
2
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT TRUE;

V3__create_orders_table.sql:

1
2
3
4
5
6
7
8
9
CREATE TABLE orders (
    order_id    SERIAL PRIMARY KEY,
    user_id     INTEGER NOT NULL REFERENCES users(user_id),
    total_amount DECIMAL(10,2) NOT NULL,
    order_date  DATE DEFAULT CURRENT_DATE
);

CREATE INDEX orders_user_id_idx ON orders(user_id);
CREATE INDEX orders_order_date_idx ON orders(order_date);

Flyway管理テーブル

Flywayはflyway_schema_historyテーブルで適用履歴を管理します。

1
2
3
SELECT version, description, installed_on, success
FROM flyway_schema_history
ORDER BY installed_rank;
version description installed_on success
1 create users table 2026-01-03 10:00:00 true
2 add phone column 2026-01-03 10:05:00 true
3 create orders table 2026-01-03 10:10:00 true

Liquibaseによるマイグレーション管理

Liquibaseは、XML、YAML、JSON、SQLなど複数のフォーマットでマイグレーションを定義できるツールです。ロールバック機能が標準で提供されている点が特徴です。

Changelog(YAML形式)の例

db.changelog-master.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
databaseChangeLog:
  - changeSet:
      id: 1
      author: developer
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: user_id
                  type: serial
                  constraints:
                    primaryKey: true
              - column:
                  name: email
                  type: varchar(255)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: name
                  type: varchar(100)
                  constraints:
                    nullable: false
              - column:
                  name: created_at
                  type: timestamp
                  defaultValueComputed: CURRENT_TIMESTAMP
      rollback:
        - dropTable:
            tableName: users

  - changeSet:
      id: 2
      author: developer
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: phone
                  type: varchar(20)
              - column:
                  name: is_active
                  type: boolean
                  defaultValueBoolean: true
      rollback:
        - dropColumn:
            tableName: users
            columnName: phone
        - dropColumn:
            tableName: users
            columnName: is_active

Liquibaseのロールバック

1
2
3
4
5
6
7
8
# 直近の変更を1つロールバック
liquibase rollbackCount 1

# 特定のタグまでロールバック
liquibase rollback v1.0

# ロールバックSQLのプレビュー
liquibase rollbackCountSQL 1

Prisma Migrateによるマイグレーション管理

Prisma Migrateは、TypeScript/JavaScript開発者向けの型安全なマイグレーションツールです。Prismaスキーマファイルから自動的にマイグレーションを生成します。

Prismaスキーマの例

prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  phone     String?
  isActive  Boolean  @default(true) @map("is_active")
  createdAt DateTime @default(now()) @map("created_at")
  orders    Order[]

  @@map("users")
}

model Order {
  id          Int      @id @default(autoincrement())
  userId      Int      @map("user_id")
  totalAmount Decimal  @map("total_amount") @db.Decimal(10, 2)
  orderDate   DateTime @default(now()) @map("order_date") @db.Date
  user        User     @relation(fields: [userId], references: [id])

  @@map("orders")
}

マイグレーションの生成と適用

1
2
3
4
5
6
7
8
# マイグレーションを生成(開発環境)
npx prisma migrate dev --name add_phone_column

# マイグレーションを本番環境に適用
npx prisma migrate deploy

# マイグレーション状態の確認
npx prisma migrate status

生成されるマイグレーションファイル(prisma/migrations/20260103_add_phone_column/migration.sql):

1
2
-- AlterTable
ALTER TABLE "users" ADD COLUMN "phone" TEXT;

本番環境でのスキーマ変更戦略

ダウンタイムを最小化するためのチェックリスト

本番環境でスキーマ変更を行う前に、以下のチェックリストを確認してください。

  1. バックアップの取得: 変更前に必ずバックアップを取得する
  2. テスト環境での検証: 本番と同等のデータ量で動作確認を行う
  3. ロック時間の見積もり: 大規模テーブルの場合、想定されるロック時間を把握する
  4. ロールバック手順の準備: 問題発生時のロールバック手順を事前に準備する
  5. メンテナンス時間帯の選定: 影響を最小化できる時間帯を選択する
  6. 監視の準備: スキーマ変更中のデータベース負荷を監視する

ロック時間を最小化するテクニック

1. 小さなバッチでのデータ更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
-- 大量データの更新を小さなバッチに分割
DO $$
DECLARE
    batch_size INT := 10000;
    affected_rows INT := 1;
BEGIN
    WHILE affected_rows > 0 LOOP
        UPDATE users
        SET status = 'migrated'
        WHERE user_id IN (
            SELECT user_id FROM users
            WHERE status IS NULL
            LIMIT batch_size
        );
        GET DIAGNOSTICS affected_rows = ROW_COUNT;
        COMMIT;
        PERFORM pg_sleep(0.1);  -- 負荷軽減のための待機
    END LOOP;
END $$;

2. 新カラム追加とバックフィルの分離

1
2
3
4
5
6
7
8
-- Step 1: NULLを許可してカラム追加(即座に完了)
ALTER TABLE users ADD COLUMN age INTEGER;

-- Step 2: バックグラウンドでデータを埋める
UPDATE users SET age = 0 WHERE age IS NULL;

-- Step 3: NOT NULL制約を追加
ALTER TABLE users ALTER COLUMN age SET NOT NULL;

3. テーブル置換パターン

既存テーブルを大幅に変更する場合、新しいテーブルを作成して置き換える方法があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- Step 1: 新しい構造のテーブルを作成
CREATE TABLE users_new (
    user_id      SERIAL PRIMARY KEY,
    email        VARCHAR(255) NOT NULL UNIQUE,
    full_name    VARCHAR(200) NOT NULL,  -- name -> full_nameに変更
    phone        VARCHAR(20),
    is_active    BOOLEAN DEFAULT TRUE,
    created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Step 2: データを移行
INSERT INTO users_new (user_id, email, full_name, phone, is_active, created_at)
SELECT user_id, email, name, phone, is_active, created_at
FROM users;

-- Step 3: シーケンスを同期
SELECT setval(
    pg_get_serial_sequence('users_new', 'user_id'),
    (SELECT MAX(user_id) FROM users_new)
);

-- Step 4: テーブルを入れ替え(短時間のロック)
BEGIN;
ALTER TABLE users RENAME TO users_old;
ALTER TABLE users_new RENAME TO users;
COMMIT;

-- Step 5: 確認後、旧テーブルを削除
DROP TABLE users_old;

ブルーグリーンデプロイメントとの連携

アプリケーションのブルーグリーンデプロイメントと連携したスキーマ変更戦略です。

sequenceDiagram
    participant Blue as Blue環境<br/>(旧バージョン)
    participant Green as Green環境<br/>(新バージョン)
    participant DB as データベース

    Note over Blue,DB: Phase 1: 両対応スキーマへ変更
    DB->>DB: 新カラム追加(NULL許可)
    Blue->>DB: 旧カラムで読み書き
    Green->>DB: 新カラムでも読み書き可能

    Note over Blue,DB: Phase 2: トラフィック切り替え
    Blue->>Green: トラフィック移行
    Green->>DB: 新カラムで読み書き

    Note over Blue,DB: Phase 3: クリーンアップ
    DB->>DB: 旧カラム削除

この戦略では、スキーマ変更を「後方互換性のある変更」と「クリーンアップ」の2段階に分けることで、無停止でのスキーマ変更を実現します。

よくある失敗パターンと対策

1. 長時間のテーブルロック

問題: 大規模テーブルへのALTER TABLEが長時間のロックを引き起こす

対策:

  • CONCURRENTLYオプション(インデックス作成時)
  • NOT VALIDオプション(制約追加時)
  • テーブル置換パターン

2. ディスク容量不足

問題: ALTER TABLE中にディスク容量が不足してエラーになる

対策:

  • 事前にテーブルサイズの2倍以上の空き容量を確保
  • 一時テーブルスペースの利用
1
2
-- テーブルサイズの確認
SELECT pg_size_pretty(pg_total_relation_size('users'));

3. 外部キー制約違反

問題: 参照先のデータが存在しないため、外部キー制約の追加が失敗する

対策:

  • 事前にデータの整合性を確認
  • 不整合データを修正してから制約を追加
1
2
3
4
5
6
7
8
9
-- 参照先が存在しない不整合データを検出
SELECT o.order_id, o.user_id
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE u.user_id IS NULL;

-- 不整合データを削除または修正してから制約を追加
DELETE FROM orders WHERE user_id NOT IN (SELECT user_id FROM users);
ALTER TABLE orders ADD CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users(user_id);

まとめ

本記事では、PostgreSQLにおけるスキーマ管理とマイグレーション戦略について解説しました。

ALTER TABLEの基本操作として、カラムの追加・削除・型変更、制約の追加・削除、名前の変更などを学びました。本番環境でのスキーマ変更では、NOT VALIDオプションやCONCURRENTLYオプションを活用してロック時間を最小化することが重要です。

マイグレーションツール(Flyway、Liquibase、Prisma Migrate)を活用することで、スキーマ変更をバージョン管理し、複数環境間での整合性を維持できます。チームでの開発や、CI/CDパイプラインへの組み込みを見据えて、早い段階からマイグレーションツールを導入することをお勧めします。

最後に、本番環境でのスキーマ変更は慎重に行う必要があります。十分なテスト、バックアップの取得、ロールバック手順の準備を怠らず、安全なスキーマ変更を実践してください。

参考リンク