設計

PostgreSQL: Enum型 vs Text + Check Constraint - どちらを選ぶべきか

PostgreSQL における Enum 型と Text + Check Constraint の比較。マイグレーションの複雑さ、テーブルロック、運用面での違いを解説します。

結論: この文章は正しい

Removing a value from an enum is surprisingly complicated and takes a toll on your database.
Instead, you can use a text column with a simple check constraint! This way the migration is much simpler, and does not require a lock on your entire table!

この主張は PostgreSQL の公式ドキュメントで裏付けられています。

Enum 型の制限

PostgreSQL の公式ドキュメント(バージョン 17)には、以下のように明記されています:

Existing values cannot be removed from an enum type, nor can the sort ordering of such values be changed, short of dropping and re-creating the enum type.

つまり、Enum 型から既存の値を削除することはできません。削除するには、型自体を削除して再作成する必要があります。

Enum の操作でできること・できないこと

操作可否コマンド例
値の追加✅ 可能ALTER TYPE colors ADD VALUE 'orange' AFTER 'red';
値のリネーム✅ 可能ALTER TYPE colors RENAME VALUE 'purple' TO 'mauve';
値の削除❌ 不可能-
順序の変更❌ 不可能-

Enum 値を削除するための複雑な手順

Enum から値を削除するには、以下のような複雑な手順が必要です:

-- 1. 依存するカラムを一時的に text に変換
ALTER TABLE users ALTER COLUMN status TYPE text;

-- 2. 古い enum 型を削除
DROP TYPE user_status;

-- 3. 新しい enum 型を作成(削除したい値を除く)
CREATE TYPE user_status AS ENUM ('active', 'inactive');
-- 'pending' を削除した例

-- 4. カラムを新しい enum 型に戻す
ALTER TABLE users ALTER COLUMN status TYPE user_status USING status::user_status;

この手順の問題点

  1. ACCESS EXCLUSIVE ロック: ALTER TABLE ... ALTER COLUMN ... TYPE は ACCESS EXCLUSIVE ロックを取得します。これはテーブル全体をロックし、すべての読み取り・書き込みをブロックします。

  2. テーブルの書き換え: カラムの型を変更する場合、テーブル全体の書き換えが発生する可能性があります。大きなテーブルでは非常に時間がかかります。

  3. ダウンタイム: ロック中は他のトランザクションがブロックされるため、本番環境ではダウンタイムが発生します。

Text + Check Constraint の利点

代わりに text カラムと CHECK 制約を使うアプローチがあります:

-- 初期作成
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    status TEXT NOT NULL CHECK (status IN ('active', 'inactive', 'pending'))
);

値を削除する場合

-- 1. 古い制約を削除
ALTER TABLE users DROP CONSTRAINT users_status_check;

-- 2. 新しい制約を追加(NOT VALID オプション付き)
ALTER TABLE users ADD CONSTRAINT users_status_check
    CHECK (status IN ('active', 'inactive')) NOT VALID;

-- 3. 後から検証(オプション)
ALTER TABLE users VALIDATE CONSTRAINT users_status_check;

NOT VALID オプションの威力

PostgreSQL の公式ドキュメントによると、NOT VALID オプションには以下の利点があります:

  1. 即座に適用: テーブルスキャンを行わず、即座に制約が追加されます
  2. 軽量なロック: VALIDATE CONSTRAINTSHARE UPDATE EXCLUSIVE ロックのみを取得し、読み取りはブロックされません
  3. 新規データへの適用: NOT VALID でも、新規挿入・更新されるデータには制約が適用されます
-- NOT VALID で制約を追加(高速、テーブルスキャンなし)
ALTER TABLE users ADD CONSTRAINT users_status_check
    CHECK (status IN ('active', 'inactive')) NOT VALID;

-- 後から検証(SHARE UPDATE EXCLUSIVE ロック = 読み取りは可能)
ALTER TABLE users VALIDATE CONSTRAINT users_status_check;

比較表

観点Enum 型Text + Check Constraint
値の追加簡単簡単
値の削除非常に複雑簡単
マイグレーション時のロックACCESS EXCLUSIVE(全ロック)SHARE UPDATE EXCLUSIVE(読み取り可能)
テーブル書き換え必要な場合あり不要
型安全性高い中程度
ストレージ効率4バイト固定可変(文字列長に依存)
インデックス効率やや良い標準

どちらを選ぶべきか

Enum 型が適している場合

  • 値が完全に固定で、削除されることがない
  • ストレージ効率が重要
  • 型レベルでの厳密な制約が必要

Text + Check Constraint が適している場合

  • 値が将来変更される可能性がある
  • ダウンタイムなしでマイグレーションしたい
  • 大規模なテーブルで運用している
  • アジャイルな開発で頻繁に変更がある

実践的な推奨

多くの本番環境では、Text + Check Constraint を推奨します。理由:

  1. 運用の柔軟性: ビジネス要件は変化するもの。値の追加・削除が容易
  2. ゼロダウンタイム: NOT VALID を使ったマイグレーションで、サービスを止めずに変更可能
  3. リスク軽減: 複雑なマイグレーション手順によるミスを防げる

Enum 型の型安全性は魅力的ですが、その代償として運用の複雑さが伴います。特に、一度作成した Enum から値を削除する必要が生じた場合、その影響は予想以上に大きくなります。

参考