Database Schema Designer
ベストプラクティスを組み込んだ、本番環境で使えるデータベーススキーマを設計します。
クイックスタート
データモデルを説明するだけです:
design a schema for an e-commerce platform with users, products, orders
次のような完全な SQL スキーマが得られます:
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total DECIMAL(10,2) NOT NULL,
INDEX idx_orders_user (user_id)
);
リクエストに含めるべき内容:
- エンティティ(ユーザー、商品、注文)
- 主要なリレーションシップ(ユーザーは注文を持つ、注文は明細を持つ)
- スケールの目安(高トラフィック、数百万件のレコード)
- データベースの希望(SQL/NoSQL)- 指定がない場合は SQL がデフォルトです
トリガー
| トリガー | 例 |
|---|
design schema | "design a schema for user authentication" |
database design | "database design for multi-tenant SaaS" |
create tables | "create tables for a blog system" |
schema for | "schema for inventory management" |
model data | "model data for real-time analytics" |
I need a database | "I need a database for tracking orders" |
design NoSQL | "design NoSQL schema for product catalog" |
主要用語
| 用語 | 定義 |
|---|
| Normalization | 冗長性を減らすためにデータを整理すること(1NF → 2NF → 3NF) |
| 3NF | 第3正規形 - 列間に推移的な従属関係がないこと |
| OLTP | オンライントランザクション処理 - 書き込みが多く、正規化が必要 |
| OLAP | オンライン分析処理 - 読み取りが多く、非正規化の恩恵を受ける |
| Foreign Key (FK) | 他のテーブルの主キーを参照する列 |
| Index | クエリを高速化するデータ構造(書き込みは遅くなる) |
| Access Pattern | アプリがデータを読み書きする方法(クエリ、結合、フィルター) |
| Denormalization | 読み取りを高速化するために意図的にデータを重複させること |
クイックリファレンス
| タスク | アプローチ | 重要な考慮事項 |
|---|
| 新規スキーマ | まず 3NF に正規化 | UI よりドメインモデリング |
| SQL vs NoSQL | アクセスパターンで決定 | 読み取り/書き込み比率が重要 |
| 主キー | INT または UUID | 分散システムには UUID |
| 外部キー | 常に制約を設定 | ON DELETE の戦略が重要 |
| インデックス | FK + WHERE 句の列 | 列の順序が重要 |
| マイグレーション | 常に可逆にする | 後方互換性を優先 |
プロセス概要
Your Data Requirements
|
v
+-----------------------------------------------------+
| Phase 1: ANALYSIS |
| * Identify entities and relationships |
| * Determine access patterns (read vs write heavy) |
| * Choose SQL or NoSQL based on requirements |
+-----------------------------------------------------+
|
v
+-----------------------------------------------------+
| Phase 2: DESIGN |
| * Normalize to 3NF (SQL) or embed/reference (NoSQL) |
| * Define primary keys and foreign keys |
| * Choose appropriate data types |
| * Add constraints (UNIQUE, CHECK, NOT NULL) |
+-----------------------------------------------------+
|
v
+-----------------------------------------------------+
| Phase 3: OPTIMIZE |
| * Plan indexing strategy |
| * Consider denormalization for read-heavy queries |
| * Add timestamps (created_at, updated_at) |
+-----------------------------------------------------+
|
v
+-----------------------------------------------------+
| Phase 4: MIGRATE |
| * Generate migration scripts (up + down) |
| * Ensure backward compatibility |
| * Plan zero-downtime deployment |
+-----------------------------------------------------+
|
v
Production-Ready Schema
コマンド
| コマンド | 使用するタイミング | アクション |
|---|
design schema for {domain} | ゼロから始めるとき | スキーマを完全に生成 |
normalize {table} | 既存テーブルの修正 | 正規化ルールを適用 |
add indexes for {table} | パフォーマンスの問題 | インデックス戦略を生成 |
migration for {change} | スキーマの進化 | 可逆なマイグレーションを作成 |
review schema | コードレビュー | 既存スキーマを監査 |
ワークフロー: design schema で開始 → normalize で反復 → add indexes で最適化 → migration で進化
コア原則
| 原則 | 理由 | 実装 |
|---|
| ドメインをモデル化する | UI は変わるが、ドメインは変わらない | エンティティ名はビジネスの概念を反映する |
| データ整合性を最優先 | 破損の修復はコストが高い | データベースレベルで制約を設定 |
| アクセスパターンに最適化 | 両方を同時に最適化することはできない | OLTP: 正規化、OLAP: 非正規化 |
| スケールを見越して計画 | 後から改修するのは大変 | インデックス戦略とパーティショニング計画 |
アンチパターン
| 避けるべきこと | 理由 | 代わりに |
|---|
| すべてに VARCHAR(255) | ストレージの無駄になり、意図が不明確になる | フィールドごとに適切なサイズを指定 |
| お金に FLOAT | 丸め誤差が生じる | DECIMAL(10,2) |
| FK 制約の欠如 | 孤立したデータが発生する | 外部キーを必ず定義する |
| FK にインデックスがない | JOIN が遅くなる | すべての外部キーにインデックスを作成 |
| 日付を文字列で保存 | 比較やソートができない | DATE、TIMESTAMP 型を使用 |
| クエリで SELECT * | 不要なデータを取得してしまう | 列を明示的に指定 |
| 不可逆なマイグレーション | ロールバックできない | 必ず DOWN マイグレーションを作成 |
| デフォルト値なしで NOT NULL を追加 | 既存の行が壊れる | nullable で追加し、値を埋めてから制約を付与 |
検証チェックリスト
スキーマを設計した後に確認します:
詳細解説: 正規化 (SQL)
正規形
| 正規形 | ルール | 違反例 |
|---|
| 第1正規形 | 原子値であり、繰り返しグループがない | product_ids = '1,2,3' |
| 第2正規形 | 第1正規形 + 部分従属がない | order_items 内の customer_name |
| 第3正規形 | 第2正規形 + 推移従属がない | postal_code から導出される country |
第1正規形 (1NF)
-- BAD: Multiple values in column
CREATE TABLE orders (
id INT PRIMARY KEY,
product_ids VARCHAR(255) -- '101,102,103'
);
-- GOOD: Separate table for items
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT
);
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT REFERENCES orders(id),
product_id INT
);
第2正規形 (2NF)
-- BAD: customer_name depends only on customer_id
CREATE TABLE order_items (
order_id INT,
product_id INT,
customer_name VARCHAR(100), -- Partial dependency!
PRIMARY KEY (order_id, product_id)
);
-- GOOD: Customer data in separate table
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(100)
);
第3正規形 (3NF)
-- BAD: country depends on postal_code
CREATE TABLE customers (
id INT PRIMARY KEY,
postal_code VARCHAR(10),
country VARCHAR(50) -- Transitive dependency!
);
-- GOOD: Separate postal_codes table
CREATE TABLE postal_codes (
code VARCHAR(10) PRIMARY KEY,
country VARCHAR(50)
);
非正規化を行う場合
| シナリオ | 非正規化の戦略 |
|---|
| 読み取りが多いレポート | 事前計算された集計値 |
| コストの高い JOIN | キャッシュされた導出列 |
| 分析ダッシュボード | マテリアライズドビュー |
-- Denormalized for performance
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT,
total_amount DECIMAL(10,2), -- Calculated
item_count INT -- Calculated
);
詳細解説: データ型
文字列型
| 型 | 用途 | 例 |
|---|
| CHAR(n) | 固定長 | 州コード、ISO 日付 |
| VARCHAR(n) | 可変長 | 名前、メールアドレス |
| TEXT | 長文コンテンツ | 記事、説明文 |
-- Good sizing
email VARCHAR(255)
phone VARCHAR(20)
country_code CHAR(2)
数値型
| 型 | 範囲 | 用途 |
|---|
| TINYINT | -128 から 127 | 年齢、ステータスコード |
| SMALLINT | -32K から 32K | 数量 |
| INT | -21億 から 21億 | ID、件数 |
| BIGINT | 非常に大きい | 大きな ID、タイムスタンプ |
| DECIMAL(p,s) | 正確な精度 | 金額 |
| FLOAT/DOUBLE | 近似値 | 科学データ |
-- ALWAYS use DECIMAL for money
price DECIMAL(10, 2) -- $99,999,999.99
-- NEVER use FLOAT for money
price FLOAT -- Rounding errors!
日付・時刻型
DATE -- 2025-10-31
TIME -- 14:30:00
DATETIME -- 2025-10-31 14:30:00
TIMESTAMP -- Auto timezone conversion
-- Always store in UTC
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
真偽値
-- PostgreSQL
is_active BOOLEAN DEFAULT TRUE
-- MySQL
is_active TINYINT(1) DEFAULT 1
詳細解説: インデックス戦略
インデックスを作成するタイミング
| 常にインデックスを作成 | 理由 |
|---|
| 外部キー | JOIN を高速化する |
| WHERE 句の列 | フィルタリングを高速化する |
| ORDER BY の列 | ソートを高速化する |
| 一意制約 | 一意性を保証する |
-- Foreign key index
CREATE INDEX idx_orders_customer ON orders(customer_id);
-- Query pattern index
CREATE INDEX idx_orders_status_date ON orders(status, created_at);
インデックスの種類
| 種類 | 最適な用途 | 例 |
|---|
| B-Tree | 範囲検索、等値検索 | price > 100 |
| Hash | 完全一致のみ | email = '[email protected]' |
| Full-text | テキスト検索 | MATCH AGAINST |
| Partial | 一部の行のみ | WHERE is_active = true |
複合インデックスの列順
CREATE INDEX idx_customer_status ON orders(customer_id, status);
-- Uses index (customer_id first)
SELECT * FROM orders WHERE customer_id = 123;
SELECT * FROM orders WHERE customer_id = 123 AND status = 'pending';
-- Does NOT use index (status alone)
SELECT * FROM orders WHERE status = 'pending';
ルール: 最も選択性の高い列を先頭にするか、単独で最も多く検索される列を先頭にします。
インデックスの落とし穴
| 落とし穴 | 問題 | 解決策 |
|---|
| インデックスの過剰作成 | 書き込みが遅くなる | 検索で使われるものだけにインデックスを作成する |
| 列順の誤り | インデックスが使われない | クエリのパターンに合わせる |
| 外部キーのインデックス欠如 | JOIN が遅くなる | 外部キーには必ずインデックスを作成する |
詳細解説: 制約
主キー
-- Auto-increment (simple)
id INT AUTO_INCREMENT PRIMARY KEY
-- UUID (distributed systems)
id CHAR(36) PRIMARY KEY DEFAULT (UUID())
-- Composite (junction tables)
PRIMARY KEY (student_id, course_id)
外部キー
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE CASCADE -- Delete children with parent
ON DELETE RESTRICT -- Prevent deletion if referenced
ON DELETE SET NULL -- Set to NULL when parent deleted
ON UPDATE CASCADE -- Update children when parent changes
| 戦略 | 使用する場面 |
|---|
| CASCADE | 従属データ(order_items など) |
| RESTRICT | 重要な参照(誤操作を防ぐ) |
| SET NULL | 任意のリレーション |
その他の制約
-- Unique
email VARCHAR(255) UNIQUE NOT NULL
-- Composite unique
UNIQUE (student_id, course_id)
-- Check
price DECIMAL(10,2) CHECK (price >= 0)
discount INT CHECK (discount BETWEEN 0 AND 100)
-- Not null
name VARCHAR(100) NOT NULL
詳細解説: リレーションのパターン
1対多
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id)
);
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INT NOT NULL,
quantity INT NOT NULL
);
多対多
-- Junction table
CREATE TABLE enrollments (
student_id INT REFERENCES students(id) ON DELETE CASCADE,
course_id INT REFERENCES courses(id) ON DELETE CASCADE,
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (student_id, course_id)
);
自己参照
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
manager_id INT REFERENCES employees(id)
);
ポリモーフィック
-- Approach 1: Separate FKs (stronger integrity)
CREATE TABLE comments (
id INT PRIMARY KEY,
content TEXT NOT NULL,
post_id INT REFERENCES posts(id),
photo_id INT REFERENCES photos(id),
CHECK (
(post_id IS NOT NULL AND photo_id IS NULL) OR
(post_id IS NULL AND photo_id IS NOT NULL)
)
);
-- Approach 2: Type + ID (flexible, weaker integrity)
CREATE TABLE comments (
id INT PRIMARY KEY,
content TEXT NOT NULL,
commentable_type VARCHAR(50) NOT NULL,
commentable_id INT NOT NULL
);
詳細解説: NoSQL 設計 (MongoDB)
埋め込みと参照の比較
| 要素 | 埋め込み | 参照 |
|---|
| アクセスパターン | 一緒に読み取る | 別々に読み取る |
| リレーション | 1:少数 | 1:多数 |
| ドキュメントサイズ | 小さい | 16MB に近づく |
| 更新頻度 | ほとんどない | 頻繁 |
Embedded Document
{
"_id": "order_123",
"customer": {
"id": "cust_456",
"name": "Jane Smith",
"email": "[email protected]"
},
"items": [
{ "product_id": "prod_789", "quantity": 2, "price": 29.99 }
],
"total": 109.97
}
Referenced Document
{
"_id": "order_123",
"customer_id": "cust_456",
"item_ids": ["item_1", "item_2"],
"total": 109.97
}
MongoDB Indexes
// Single field
db.users.createIndex({ email: 1 }, { unique: true });
// Composite
db.orders.createIndex({ customer_id: 1, created_at: -1 });
// Text search
db.articles.createIndex({ title: "text", content: "text" });
// Geospatial
db.stores.createIndex({ location: "2dsphere" });
Deep Dive: マイグレーション
マイグレーションのベストプラクティス
| プラクティス | 理由 |
|---|
| 常に可逆にする | ロールバックが必要になるため |
| 後方互換性を保つ | ダウンタイムなしのデプロイのため |
| データより先にスキーマを変更する | 関心事を分離するため |
| ステージング環境でテストする | 問題を早期に発見するため |
カラムの追加(ダウンタイムなし)
-- Step 1: Add nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Step 2: Deploy code that writes to new column
-- Step 3: Backfill existing rows
UPDATE users SET phone = '' WHERE phone IS NULL;
-- Step 4: Make required (if needed)
ALTER TABLE users MODIFY phone VARCHAR(20) NOT NULL;
カラム名の変更(ダウンタイムなし)
-- Step 1: Add new column
ALTER TABLE users ADD COLUMN email_address VARCHAR(255);
-- Step 2: Copy data
UPDATE users SET email_address = email;
-- Step 3: Deploy code reading from new column
-- Step 4: Deploy code writing to new column
-- Step 5: Drop old column
ALTER TABLE users DROP COLUMN email;
マイグレーションのテンプレート
-- Migration: YYYYMMDDHHMMSS_description.sql
-- UP
BEGIN;
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
CREATE INDEX idx_users_phone ON users(phone);
COMMIT;
-- DOWN
BEGIN;
DROP INDEX idx_users_phone ON users;
ALTER TABLE users DROP COLUMN phone;
COMMIT;
Deep Dive: パフォーマンス最適化
クエリ分析
EXPLAIN SELECT * FROM orders
WHERE customer_id = 123 AND status = 'pending';
| 確認すべき項目 | 意味 |
|---|
| type: ALL | フルテーブルスキャン(好ましくない) |
| type: ref | インデックスを使用(好ましい) |
| key: NULL | インデックスが使用されていない |
| rows: high | 多くの行をスキャンしている |
N+1 クエリ問題
# BAD: N+1 queries
orders = db.query("SELECT * FROM orders")
for order in orders:
customer = db.query(f"SELECT * FROM customers WHERE id = {order.customer_id}")
# GOOD: Single JOIN
results = db.query("""
SELECT orders.*, customers.name
FROM orders
JOIN customers ON orders.customer_id = customers.id
""")
最適化テクニック
| テクニック | 使用するタイミング |
|---|
| インデックスを追加する | WHERE/ORDER BY が遅い場合 |
| 非正規化する | 高コストな JOIN がある場合 |
| ページネーション | 大量の結果セットを扱う場合 |
| キャッシュ | 同じクエリが繰り返し実行される場合 |
| リードレプリカ | 読み取り負荷が高い場合 |
| パーティショニング | 非常に大きなテーブルの場合 |
拡張ポイント
- データベース固有のパターン: MySQL、PostgreSQL、SQLite それぞれのバリエーションを追加する
- 高度なパターン: 時系列データ、イベントソーシング、CQRS、マルチテナンシー
- ORM 統合: TypeORM、Prisma、SQLAlchemy のパターン
- モニタリング: クエリパフォーマンスの追跡、スロークエリのアラート