データモデル設計ガイド

ENUM vs ルックアップテーブル vs CHECK 制約

限定値集合を ENUM / ルックアップテーブル / CHECK 制約で表現する 3 択の比較。

ENUM vs ルックアップテーブル vs CHECK 制約 diagram

「限定された値の集合」をどう持つか

「ステータスは 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 のバッファキャッシュに載り、実測コストは無視できる範囲です。

sql
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 では強制されないことを知っておきます。

sql
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 で済むため、ゼロダウンタイムリリースと相性が良いです。