ENUM vs ルックアップテーブル vs CHECK 制約
限定値集合を ENUM / ルックアップテーブル / CHECK 制約で表現する 3 択の比較。
「限定された値の集合」をどう持つか
「ステータスは pending / paid / cancelled のいずれか」のような限定された値の集合をスキーマ上で強制する方法は 3 つあります。
- ENUM 型: PostgreSQL / MySQL がネイティブサポート
- ルックアップテーブル: 別テーブルに値を持ち、FK で参照
- CHECK 制約:
CHECK (status IN ('pending','paid','cancelled'))
どれを選ぶかは「値の集合がどれくらい変わるか」と「多言語表示や並び順、追加属性が必要か」で決まります。
ENUM 型 — 手軽だが拡張に弱い
ENUM はスキーマ定義に値集合を埋め込む方式です。
- PostgreSQL:
CREATE TYPE order_status AS ENUM ('pending','paid','cancelled');。追加はALTER TYPE ... ADD VALUEで可能だが、削除・並べ替えは非常に面倒(新しい TYPE を作って移行) - MySQL:
status ENUM('pending','paid','cancelled')。値の追加はALTER TABLEで、大きなテーブルではテーブル再作成になりロックが走る - SQLite / SQL Server: ENUM 型はない(CHECK 制約で代用)
利点: 短い、高速、ストレージ効率が良い。値が 5 年間変わらないと確信できるならアリ。追加属性が要らない単純な状態(boolean 的な 3〜4 値)に向きます。
ルックアップテーブル — 柔軟だが JOIN が増える
別テーブルに値を持ち、本体テーブルから外部キーで参照する方式です。
利点:
- 表示ラベル、並び順、i18n、アイコンなどの追加属性を持てる
- 値の追加・変更が通常の
INSERT/UPDATEで済む(スキーマ変更不要) - 外部キー制約で整合性が守られる
欠点: クエリに必ず JOIN が 1 つ増える。ただし十数件程度のマスタなら PostgreSQL / MySQL のバッファキャッシュに載り、実測コストは無視できる範囲です。
CREATE TABLE order_statuses (
code VARCHAR(20) PRIMARY KEY, -- 'pending' 等 (自然キー)
label_ja VARCHAR(50) NOT NULL,
label_en VARCHAR(50) NOT NULL,
sort_order INT NOT NULL
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status VARCHAR(20) NOT NULL,
FOREIGN KEY (status) REFERENCES order_statuses(code)
);CHECK 制約 — どの RDBMS でも使える最小解
CHECK (status IN (...)) はどの RDBMS でも使えて実装が最も簡単です。ENUM のポータビリティ問題を回避しつつ、ルックアップテーブルほどの JOIN コストも払いません。
欠点は、値を追加するときにアプリ側のコードと CHECK 制約の両方を変更する必要があることと、表示ラベル等の付加情報を持てないこと。アプリ側のコードで status ↔ 表示名の対応を管理することになります。
ちなみに MySQL は 8.0.16 以降で CHECK 制約が実際に強制されるようになったばかりです(それ以前はパースするが無視)。古い MySQL では強制されないことを知っておきます。
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status VARCHAR(20) NOT NULL
CHECK (status IN ('pending','paid','cancelled'))
);選び分けの指針
実務の目安:
- 値が変わらず、付加情報も不要 → CHECK 制約(最小コスト、移植性高)
- 多言語ラベル・並び順・追加属性が必要 → ルックアップテーブル
- PostgreSQL で型レベルの厳密さがほしく、値が安定 → ENUM 型
- MySQL で大テーブルに ENUM を足す → やめる。ルックアップか CHECK にする
個人的には迷ったらルックアップテーブルが無難です。後から属性を足せる柔軟性は大きく、JOIN コストは現代のマシンスペックなら通常問題にならないからです。
ENUM と CHECK の共通の罠
ENUM も CHECK も、値の集合がスキーマに埋め込まれているため、アプリのデプロイと DDL 変更のタイミングをずらせません。新しい値 'refunded' を使うコードを先にデプロイすると、DDL が追いつくまでエラーになります。
逆に DDL を先に入れると、古いアプリが新しい値を知らずに落ちます。ルックアップテーブルなら値の追加は単なる INSERT で済むため、ゼロダウンタイムリリースと相性が良いです。
