ストーリー
「価格カラムをFLOATにしてる? これは事故の元だぞ」
高橋アーキテクトがコードレビューで指摘した。0.1 + 0.2 が 0.30000000000000004 になる浮動小数点の罠。金額計算で1円のずれが累積し、決算時に大問題になった事例を聞かされた。
「データ型の選択は、ビジネスの正確性を左右する。適当に選ぶと、後で泣くことになる」
数値型の選択
整数型
| 型 | サイズ | 範囲 | 用途 |
|---|
| SMALLINT | 2byte | -32,768 ~ 32,767 | 年齢、ステータスコード |
| INTEGER | 4byte | -2.1億 ~ 2.1億 | ID、カウンター |
| BIGINT | 8byte | -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 NULL | NULL禁止 | 必須フィールド |
| 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()
);
| 特性 | SERIAL | UUID |
|---|
| サイズ | 4-8 byte | 16 byte |
| 順序性 | あり | なし(UUIDv7は時系列順) |
| 推測可能性 | 容易 | 困難 |
| 分散環境 | 衝突リスク | 衝突なし |
| インデックス効率 | 高い | やや低い |
まとめ
| カテゴリ | 推奨 |
|---|
| 金額 | DECIMAL(p,s)(FLOATは使わない) |
| ID | SERIAL(単一DB)/ UUID(分散環境) |
| 文字列 | VARCHAR(n) でnはビジネスルールから |
| 日時 | TIMESTAMPTZ(タイムゾーン付き) |
| 状態 | ENUM または CHECK制約 |
| 制約 | 可能な限り DB レベルで強制する |
理解度チェックリスト
次のステップ
次のレッスンでは、データ設計でやりがちなアンチパターンを学ぶ。EAV、ポリモーフィック関連など、一見便利だが危険な設計パターンを見破る力を身につけよう。
推定読了時間: 25分