データモデリング - 効率的なデータベース設計

17分 で読める | 2025.01.10

公式ドキュメント

データモデリングの3段階

段階目的成果物
概念モデルビジネス要件の理解エンティティ関連図
論理モデルデータ構造の定義ER図、属性定義
物理モデル実装の最適化テーブル定義、インデックス

ER図の基本

erDiagram
 User ||--o{ Order : places
 Order ||--|{ OrderItem : contains
 Product ||--o{ OrderItem : "included in"
 Category ||--o{ Product : contains

 User {
 uuid id PK
 string email UK
 string name
 datetime created_at
 }

 Order {
 uuid id PK
 uuid user_id FK
 decimal total_amount
 string status
 datetime ordered_at
 }

 OrderItem {
 uuid id PK
 uuid order_id FK
 uuid product_id FK
 int quantity
 decimal unit_price
 }

 Product {
 uuid id PK
 uuid category_id FK
 string name
 decimal price
 int stock
 }

ポイント: 正規化は第3正規形(3NF)まで適用するのが実務の目安です。それ以上は理論的には正しくても、実装の複雑さに見合わないことが多いです。

正規化

第1正規形(1NF)

繰り返しグループを排除し、各カラムは原子値のみを持つ。

-- 違反例
CREATE TABLE orders (
 id INT,
 products TEXT -- 'Product A, Product B, Product C'
);

-- 1NF適用後
CREATE TABLE orders (
 id INT PRIMARY KEY
);

CREATE TABLE order_items (
 id INT PRIMARY KEY,
 order_id INT REFERENCES orders(id),
 product_name TEXT
);

第2正規形(2NF)

部分関数従属を排除(複合キーの一部への依存をなくす)。

-- 違反例(product_nameはproduct_idのみに依存)
CREATE TABLE order_items (
 order_id INT,
 product_id INT,
 product_name TEXT, -- 部分従属
 quantity INT,
 PRIMARY KEY (order_id, product_id)
);

-- 2NF適用後
CREATE TABLE products (
 id INT PRIMARY KEY,
 name TEXT
);

CREATE TABLE order_items (
 order_id INT,
 product_id INT REFERENCES products(id),
 quantity INT,
 PRIMARY KEY (order_id, product_id)
);

第3正規形(3NF)

推移的関数従属を排除。

-- 違反例(category_nameはcategory_idを経由して依存)
CREATE TABLE products (
 id INT PRIMARY KEY,
 name TEXT,
 category_id INT,
 category_name TEXT -- 推移的従属
);

-- 3NF適用後
CREATE TABLE categories (
 id INT PRIMARY KEY,
 name TEXT
);

CREATE TABLE products (
 id INT PRIMARY KEY,
 name TEXT,
 category_id INT REFERENCES categories(id)
);

注意: 非正規化はデータ不整合のリスクを伴います。更新時に複数箇所を変更する必要があるため、トランザクション内で整合性を保つ仕組みが必須です

非正規化

パフォーマンスのために意図的に正規化を崩すケース。

-- 正規化(JOINが必要)
SELECT o.id, u.name, o.total_amount
FROM orders o
JOIN users u ON o.user_id = u.id;

-- 非正規化(読み取り高速化)
CREATE TABLE orders (
 id INT PRIMARY KEY,
 user_id INT,
 user_name TEXT, -- 非正規化フィールド
 total_amount DECIMAL
);

-- トレードオフ
-- ✅ 読み取りが高速
-- ❌ データ更新時に複数箇所を変更
-- ❌ データ不整合のリスク

非正規化パターン

// 集計値の保持
interface Product {
 id: string;
 name: string;
 reviewCount: number; // 非正規化
 averageRating: number; // 非正規化
}

// レビュー追加時に更新
async function addReview(productId: string, rating: number) {
 await db.transaction(async (tx) => {
 // レビュー追加
 await tx.review.create({ productId, rating });

 // 集計値を更新
 const stats = await tx.review.aggregate({
 where: { productId },
 _avg: { rating: true },
 _count: true,
 });

 await tx.product.update({
 where: { id: productId },
 data: {
 reviewCount: stats._count,
 averageRating: stats._avg.rating,
 },
 });
 });
}

ドメイン駆動設計との連携

集約ルート

// 集約ルート: Order
interface Order {
 id: OrderId;
 customerId: CustomerId;
 items: OrderItem[]; // 集約内のエンティティ
 status: OrderStatus;

 addItem(product: Product, quantity: number): void;
 removeItem(itemId: OrderItemId): void;
 calculateTotal(): Money;
}

// リポジトリは集約単位で操作
interface OrderRepository {
 findById(id: OrderId): Promise<Order | null>;
 save(order: Order): Promise<void>;
}

値オブジェクト

class Money {
 constructor(
 private readonly amount: number,
 private readonly currency: string
 ) {
 if (amount < 0) throw new Error('Amount cannot be negative');
 }

 add(other: Money): Money {
 if (this.currency !== other.currency) {
 throw new Error('Currency mismatch');
 }
 return new Money(this.amount + other.amount, this.currency);
 }

 equals(other: Money): boolean {
 return this.amount === other.amount && this.currency === other.currency;
 }
}

スキーマ設計パターン

継承パターン

-- シングルテーブル継承
CREATE TABLE payments (
 id UUID PRIMARY KEY,
 type VARCHAR(20), -- 'credit_card', 'bank_transfer', 'paypal'
 amount DECIMAL,
 -- クレジットカード用
 card_number VARCHAR(16),
 card_expiry DATE,
 -- 銀行振込用
 bank_code VARCHAR(10),
 account_number VARCHAR(20),
 -- PayPal用
 paypal_email VARCHAR(255)
);

-- クラステーブル継承(推奨)
CREATE TABLE payments (
 id UUID PRIMARY KEY,
 type VARCHAR(20),
 amount DECIMAL
);

CREATE TABLE credit_card_payments (
 id UUID PRIMARY KEY REFERENCES payments(id),
 card_number VARCHAR(16),
 card_expiry DATE
);

CREATE TABLE bank_transfers (
 id UUID PRIMARY KEY REFERENCES payments(id),
 bank_code VARCHAR(10),
 account_number VARCHAR(20)
);

実践メモ: ソフトデリートを使う場合、ユニーク制約はWHERE条件付き(PostgreSQLのPartial Index等)にしないと、削除済みデータと衝突します。

ソフトデリート

CREATE TABLE users (
 id UUID PRIMARY KEY,
 email VARCHAR(255),
 deleted_at TIMESTAMP, -- NULLなら有効

 CONSTRAINT uk_email UNIQUE (email) WHERE deleted_at IS NULL
);

-- 削除
UPDATE users SET deleted_at = NOW() WHERE id = '...';

-- 検索時
SELECT * FROM users WHERE deleted_at IS NULL;

関連記事

まとめ

データモデリングは、ビジネス要件の理解から始まり、正規化で整合性を確保し、必要に応じて非正規化でパフォーマンスを最適化します。ドメイン駆動設計と組み合わせることで、保守性の高い設計が実現できます。

参考リソース

← 一覧に戻る
PR
PR
PR
PR