型選びの実務 — VARCHAR/TEXT/NUMERIC/JSON の使いどころ
VARCHAR(N) / TEXT / NUMERIC / JSON の選び方と、金額を FLOAT にしてはいけない理由。
VARCHAR(N) の N に意味はあるのか
VARCHAR(N) の N は「最大文字数」を意味しますが、内部ストレージには (ほぼ) 影響しません。PostgreSQL では VARCHAR・VARCHAR(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: 物理量、統計、機械学習の特徴量など、多少の誤差が許容される数値にのみ使う
-- 悪い例
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 を張れない
- 「複数行」をネストしたくなる衝動 → それは正規化の出番
-- 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 を文字列で持つ場合など) に限るくらいの割り切りで構いません。
