結論: この文章は正しい
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;この手順の問題点
-
ACCESS EXCLUSIVE ロック:
ALTER TABLE ... ALTER COLUMN ... TYPEは ACCESS EXCLUSIVE ロックを取得します。これはテーブル全体をロックし、すべての読み取り・書き込みをブロックします。 -
テーブルの書き換え: カラムの型を変更する場合、テーブル全体の書き換えが発生する可能性があります。大きなテーブルでは非常に時間がかかります。
-
ダウンタイム: ロック中は他のトランザクションがブロックされるため、本番環境ではダウンタイムが発生します。
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 オプションには以下の利点があります:
- 即座に適用: テーブルスキャンを行わず、即座に制約が追加されます
- 軽量なロック:
VALIDATE CONSTRAINTはSHARE UPDATE EXCLUSIVEロックのみを取得し、読み取りはブロックされません - 新規データへの適用:
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 を推奨します。理由:
- 運用の柔軟性: ビジネス要件は変化するもの。値の追加・削除が容易
- ゼロダウンタイム: NOT VALID を使ったマイグレーションで、サービスを止めずに変更可能
- リスク軽減: 複雑なマイグレーション手順によるミスを防げる
Enum 型の型安全性は魅力的ですが、その代償として運用の複雑さが伴います。特に、一度作成した Enum から値を削除する必要が生じた場合、その影響は予想以上に大きくなります。