データモデル設計ガイド

型選びの実務 — VARCHAR/TEXT/NUMERIC/JSON の使いどころ

VARCHAR(N) / TEXT / NUMERIC / JSON の選び方と、金額を FLOAT にしてはいけない理由。

型選びの実務 — VARCHAR/TEXT/NUMERIC/JSON の使いどころ diagram

VARCHAR(N) の N に意味はあるのか

VARCHAR(N) の N は「最大文字数」を意味しますが、内部ストレージには (ほぼ) 影響しません。PostgreSQL では VARCHARVARCHAR(N)TEXT実装がほぼ同一で、N はチェック制約に相当するだけです。

  • PostgreSQL: どれもほぼ同じ。TEXT を使うか、CHECK (length(col) <= 100) の方が柔軟
  • MySQL: VARCHAR は行内に保存、TEXT はオーバーフローページ。使い分けに意味がある
  • SQL Server: VARCHAR(N)(〜8000)と VARCHAR(MAX) でインデックス可否などが変わる

RDBMS によって差があるため、「VARCHAR(255) にしておけば安心」は PostgreSQL では無意味、MySQL では意味ありです。

TEXT で問題ないか — 実務の判断

PostgreSQL なら 基本 TEXT で OK。長さ制限は業務要件なので CHECK 制約で書く方がドキュメント性が高い。

MySQL では:

  • 数百文字以内の短い文字列(名前、メール、URL 等): VARCHAR(N) が適切(行内保存でインデックスが使える)
  • 長文(記事本文、コメント等): TEXT / MEDIUMTEXT / LONGTEXT
  • インデックスを張る列は必ず VARCHAR にする(TEXT のフルインデックスには prefix 指定が必要)

「とりあえず VARCHAR(255) 」習慣は歴史的に MySQL のインデックスキー長制限 (767 バイト) 由来で、utf8mb4 で 191 文字ギリギリという事情があったものです。現在は innodb_large_prefix で緩和されていますが、文化として残っています。

NUMERIC vs FLOAT — 通貨は絶対に FLOAT 禁止

FLOAT / DOUBLE は2 進浮動小数で、0.1 + 0.2 = 0.30000000000000004 のような誤差が出ます。通貨・税金・会計のような10 進での厳密計算が必要な場面では絶対に使ってはいけません

正しい選択肢:

  • NUMERIC(P, S) / DECIMAL(P, S): 任意精度の 10 進小数。NUMERIC(12, 2) で 10 桁整数部 + 2 桁小数部 = 最大 99,999,999,999.99。通貨に最適
  • 整数 (BIGINT) で「最小単位」を保存: 円なら整数円、ドルならセント整数。計算が最速で誤差ゼロ。暗号資産系で広く使われる
  • FLOAT / DOUBLE: 物理量、統計、機械学習の特徴量など、多少の誤差が許容される数値にのみ使う
sql
-- 悪い例
price FLOAT  -- 誤差が出る

-- 良い例 1: NUMERIC
price NUMERIC(12, 2) NOT NULL

-- 良い例 2: 最小単位の整数
price_cents BIGINT NOT NULL  -- 1050 は $10.50

整数型の幅 — INT か BIGINT か

32 bit INT の上限は約 21 億。Web サービスの主キーで 21 億行を超えることは現実的にあり得るため、主キー・外部キーは最初から BIGINT にするのが安全です。

  • INT で始めて溢れそうになると、ALTER で BIGINT 化する際に巨大テーブルの書き換え + 子テーブルの FK も全部変更という大手術になる
  • 8 バイト使うので 4 バイトより広いが、現代のスペックではほぼ無視できる差
  • ちょっとしたルックアップテーブル (国コード等 200 行くらい) なら SMALLINT / INT でも問題ない

2016 年の Twitter のように、ID 溢れで深夜緊急対応になる事例は後を絶ちません。主キーは BIGINT 一択

JSON 列の使いどころ

PostgreSQL の JSONB、MySQL 5.7+ の JSON、SQL Server の NVARCHAR(MAX) + JSON 関数 により、半構造化データを 1 列で持てるようになりました。スキーマを固定できないデータには強力ですが、リレーショナルの代わりに使うと負債化します。

JSON を使うべき場面:

  • 外部 API のレスポンス生データを監査用に保存
  • ユーザーごとにキーが変わる設定値 (key-value ストア的な用途)
  • イベントのペイロード(構造がイベント種別ごとに違う)
  • 試作段階で変わり続ける属性を一時的に JSON で持ち、安定してきたら列に昇格

避けるべき場面:

  • 頻繁に検索・集計したい属性 → 通常の列にする。JSON 索引は作れるが通常の B-Tree より非効率
  • 整合性を担保したい参照関係 → JSON 中の ID には FK を張れない
  • 「複数行」をネストしたくなる衝動 → それは正規化の出番
sql
-- PostgreSQL: JSONB + 部分インデックス
CREATE TABLE events (
  id         BIGSERIAL   PRIMARY KEY,
  type       VARCHAR(50) NOT NULL,
  payload    JSONB       NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- よく検索する属性にだけ JSONB インデックス
CREATE INDEX events_user ON events ((payload->>'user_id'));

CHAR(N) の出番は意外に少ない

CHAR(N) は「固定長」で、足りない分は空白パディングされます。長さが本当に固定の値(例: ISO 国コード 2 文字、性別 1 文字、日本の郵便番号ハイフンなし 7 桁)には理論上適していますが、空白パディングの挙動が RDBMS やクライアントで揺れるため、実運用では VARCHAR の方が事故が少ないです。

  • PostgreSQL: CHAR(5) に 'JP' を入れると 'JP ' として保存される。比較時は空白トリムされる (SQL 標準)
  • MySQL: デフォルトで末尾空白を保存せず返す (PAD_CHAR_TO_FULL_LENGTH で変更可)

実務では ほぼすべて VARCHAR / TEXT で良く、CHAR は固定長バイナリ (UUID を文字列で持つ場合など) に限るくらいの割り切りで構いません。