LESSON 25分

ストーリー

「価格カラムをFLOATにしてる? これは事故の元だぞ」

高橋アーキテクトがコードレビューで指摘した。0.1 + 0.2 が 0.30000000000000004 になる浮動小数点の罠。金額計算で1円のずれが累積し、決算時に大問題になった事例を聞かされた。

「データ型の選択は、ビジネスの正確性を左右する。適当に選ぶと、後で泣くことになる」


数値型の選択

整数型

サイズ範囲用途
SMALLINT2byte-32,768 ~ 32,767年齢、ステータスコード
INTEGER4byte-2.1億 ~ 2.1億ID、カウンター
BIGINT8byte-922京 ~ 922京大量データのID、タイムスタンプ

小数型

-- NG: 金額にFLOATを使う
CREATE TABLE orders_bad (
  total FLOAT  -- 0.1 + 0.2 = 0.30000000000000004
);

-- OK: DECIMALで正確な計算
CREATE TABLE orders (
  total DECIMAL(12, 2) NOT NULL  -- 最大9999999999.99
);
精度用途
DECIMAL(p,s)正確金額、税率、割合
FLOAT/REAL近似科学計算、座標
DOUBLE近似(高精度)統計処理

文字列型の選択

-- 固定長 vs 可変長
CREATE TABLE example (
  country_code CHAR(2) NOT NULL,    -- 固定長: 'JP', 'US'
  name VARCHAR(100) NOT NULL,        -- 可変長: 長さが変動
  description TEXT                    -- 長文テキスト
);
用途注意点
CHAR(n)固定長コード(国コード、通貨コード)パディングされる
VARCHAR(n)一般的な文字列nの適切な設定が重要
TEXT長文(制限なし)インデックスに制限あり

VARCHAR(n) の n をどう決めるか

// ビジネスルールから決める
const FIELD_LIMITS = {
  userName: 100,      // 実用上十分
  email: 255,         // RFC 5321の上限
  phoneNumber: 20,    // 国際番号含む
  postalCode: 10,     // 日本: 8文字、国際対応
  url: 2048,          // ブラウザの実用上限
  slug: 200,          // URL用識別子
} as const;

日付・時刻型

CREATE TABLE events (
  event_date DATE NOT NULL,           -- 日付のみ: 2024-03-15
  start_time TIME NOT NULL,           -- 時刻のみ: 14:30:00
  created_at TIMESTAMP NOT NULL       -- 日時: 2024-03-15 14:30:00
    DEFAULT NOW(),
  scheduled_at TIMESTAMPTZ NOT NULL   -- タイムゾーン付き(推奨)
);
用途推奨場面
DATE日付のみ誕生日、期限日
TIME時刻のみ営業時間
TIMESTAMP日時作成日時、更新日時
TIMESTAMPTZタイムゾーン付き日時国際サービス(常にこれを推奨)

Boolean と ENUM

-- Boolean
CREATE TABLE users (
  is_active BOOLEAN NOT NULL DEFAULT true,
  is_verified BOOLEAN NOT NULL DEFAULT false
);

-- ENUM(PostgreSQL)
CREATE TYPE order_status AS ENUM (
  'pending', 'confirmed', 'shipped', 'delivered', 'cancelled'
);

CREATE TABLE orders (
  status order_status NOT NULL DEFAULT 'pending'
);

TypeScript での対応

// Booleanフラグ
interface User {
  isActive: boolean;
  isVerified: boolean;
}

// ENUM → Union Type
type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';

interface Order {
  status: OrderStatus;
}

制約(Constraints)

制約はデータの品質を守る防壁。

CREATE TABLE products (
  id SERIAL PRIMARY KEY,

  -- NOT NULL: 必須フィールド
  name VARCHAR(200) NOT NULL,

  -- UNIQUE: 重複を許さない
  sku VARCHAR(50) NOT NULL UNIQUE,

  -- CHECK: 値の範囲を制限
  price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
  stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),

  -- FOREIGN KEY: 参照整合性
  category_id INT NOT NULL REFERENCES categories(id),

  -- 複合UNIQUE制約
  UNIQUE (name, category_id)
);

制約の一覧

制約目的
PRIMARY KEY行の一意識別id
NOT NULLNULL禁止必須フィールド
UNIQUE重複禁止email, sku
CHECK値の検証price >= 0
FOREIGN KEY参照整合性user_id → users.id
DEFAULTデフォルト値created_at DEFAULT NOW()

FK の ON DELETE / ON UPDATE

-- CASCADE: 親削除 → 子も削除
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE

-- SET NULL: 親削除 → NULLにする
FOREIGN KEY (manager_id) REFERENCES users(id) ON DELETE SET NULL

-- RESTRICT: 子がある限り親を削除できない(デフォルト)
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE RESTRICT

UUID vs SERIAL

-- SERIAL: 連番(シンプル、高速)
CREATE TABLE users_serial (
  id SERIAL PRIMARY KEY
);

-- UUID: 分散環境向け
CREATE TABLE users_uuid (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid()
);
特性SERIALUUID
サイズ4-8 byte16 byte
順序性ありなし(UUIDv7は時系列順)
推測可能性容易困難
分散環境衝突リスク衝突なし
インデックス効率高いやや低い

まとめ

カテゴリ推奨
金額DECIMAL(p,s)(FLOATは使わない)
IDSERIAL(単一DB)/ UUID(分散環境)
文字列VARCHAR(n) でnはビジネスルールから
日時TIMESTAMPTZ(タイムゾーン付き)
状態ENUM または CHECK制約
制約可能な限り DB レベルで強制する

理解度チェックリスト

  • 金額にFLOATではなくDECIMALを使う理由を説明できる
  • VARCHAR(n)のnの決め方を理解している
  • 各制約(PK, FK, UNIQUE, CHECK, NOT NULL)の役割を説明できる
  • UUID と SERIAL の使い分けを判断できる

次のステップ

次のレッスンでは、データ設計でやりがちなアンチパターンを学ぶ。EAV、ポリモーフィック関連など、一見便利だが危険な設計パターンを見破る力を身につけよう。


推定読了時間: 25分