データベース設計ベストプラクティス完全ガイド – 正規化からパフォーマンス最適化まで

 

効率的で保守性の高いシステムを構築するために、適切なデータベース設計は極めて重要です。本記事では、データベース設計の基本原則から実践的なテクニックまで、長期的に成功するデータベース設計のベストプラクティスを詳しく解説します。

データベース設計の基本原則

データベース設計では、データの整合性、パフォーマンス、保守性を両立させることが重要です。ACID特性(原子性、一貫性、独立性、永続性)を満たしつつ、ビジネス要件に適した構造を設計する必要があります。

正規化の原則と実践

1. 第一正規形(1NF)

各セルには単一の値のみを格納し、繰り返しグループを排除します。

-- 悪い例:繰り返しグループ
CREATE TABLE customers_bad (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    phone1 VARCHAR(20),
    phone2 VARCHAR(20),
    phone3 VARCHAR(20)
);

-- 良い例:正規化
CREATE TABLE customers (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE customer_phones (
    customer_id INT,
    phone VARCHAR(20),
    phone_type ENUM('mobile', 'home', 'work'),
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

2. 第二正規形(2NF)

部分的関数従属を排除します。

-- 悪い例:部分的関数従属
CREATE TABLE order_items_bad (
    order_id INT,
    product_id INT,
    product_name VARCHAR(100),  -- product_idのみに依存
    quantity INT,
    PRIMARY KEY (order_id, product_id)
);

-- 良い例:分離
CREATE TABLE orders (
    id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE
);

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10,2)
);

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

3. 第三正規形(3NF)

推移的関数従属を排除します。

-- 悪い例:推移的従属
CREATE TABLE employees_bad (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    department_id INT,
    department_name VARCHAR(100),  -- department_idに推移的に依存
    department_location VARCHAR(100)
);

-- 良い例:分離
CREATE TABLE departments (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    location VARCHAR(100)
);

CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    department_id INT,
    FOREIGN KEY (department_id) REFERENCES departments(id)
);

主キーとユニークキーの設計

1. 自然キー vs 代理キー

-- 代理キー(推奨):変更されない、システム生成
CREATE TABLE customers (
    id INT AUTO_INCREMENT PRIMARY KEY,  -- 代理キー
    email VARCHAR(255) UNIQUE NOT NULL,  -- 自然キー(一意制約)
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. 複合主キー

-- 適切な複合主キー
CREATE TABLE user_roles (
    user_id INT,
    role_id INT,
    assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (role_id) REFERENCES roles(id)
);

3. UUID の使用

-- 分散システムでのUUID使用
CREATE TABLE orders (
    id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
    customer_id INT,
    total_amount DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

外部キー制約と参照整合性

1. 基本的な外部キー

CREATE TABLE orders (
    id INT PRIMARY KEY,
    customer_id INT NOT NULL,
    status ENUM('pending', 'completed', 'cancelled') DEFAULT 'pending',
    FOREIGN KEY (customer_id) REFERENCES customers(id)
        ON DELETE RESTRICT 
        ON UPDATE CASCADE
);

2. カスケード操作の適切な使用

-- 親削除時に子も削除(ログテーブルなど)
CREATE TABLE audit_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    action VARCHAR(100),
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) 
        ON DELETE CASCADE
);

3. ソフト削除の実装

CREATE TABLE customers (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(255),
    deleted_at TIMESTAMP NULL,
    INDEX idx_active_customers (deleted_at)  -- NULLインデックス
);

-- アクティブな顧客のみ取得
SELECT * FROM customers WHERE deleted_at IS NULL;

インデックス設計戦略

1. 基本的なインデックス設計

-- 検索頻度の高いカラム
CREATE INDEX idx_customers_email ON customers(email);
CREATE INDEX idx_orders_date ON orders(order_date);

-- 複合インデックス(カーディナリティ順)
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);

2. 部分インデックス

-- 条件付きインデックス(PostgreSQL)
CREATE INDEX idx_active_orders 
ON orders(customer_id, order_date) 
WHERE status = 'active';

3. 全文検索インデックス

-- MySQL全文検索
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,
    FULLTEXT(title, content)
);

-- 検索クエリ
SELECT * FROM articles 
WHERE MATCH(title, content) AGAINST('データベース 設計' IN BOOLEAN MODE);

データ型の選択とストレージ最適化

1. 適切なデータ型選択

CREATE TABLE products (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,  -- 可変長文字列
    price DECIMAL(10,2) NOT NULL,  -- 正確な小数点計算
    weight FLOAT,  -- 近似値で十分
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. ENUM vs 参照テーブル

-- 小規模で変更頻度の低い値はENUM
CREATE TABLE orders (
    id INT PRIMARY KEY,
    status ENUM('pending', 'processing', 'shipped', 'delivered') DEFAULT 'pending'
);

-- 複雑で変更可能な値は参照テーブル
CREATE TABLE order_statuses (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    description TEXT
);

CREATE TABLE orders_flexible (
    id INT PRIMARY KEY,
    status_id INT,
    FOREIGN KEY (status_id) REFERENCES order_statuses(id)
);

3. JSON データ型の活用

-- 構造化されていないデータの格納
CREATE TABLE user_preferences (
    user_id INT PRIMARY KEY,
    settings JSON,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- JSON データの検索
SELECT * FROM user_preferences 
WHERE JSON_EXTRACT(settings, '$.theme') = 'dark';

テーブル分割とパーティショニング

1. 水平分割(パーティショニング)

-- PostgreSQL の範囲パーティショニング
CREATE TABLE sales (
    id SERIAL,
    sale_date DATE,
    amount DECIMAL(10,2)
) PARTITION BY RANGE (sale_date);

CREATE TABLE sales_2024_q1 PARTITION OF sales
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE TABLE sales_2024_q2 PARTITION OF sales
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');

2. 垂直分割

-- 基本情報テーブル
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    email VARCHAR(255),
    created_at TIMESTAMP
);

-- 拡張情報テーブル(アクセス頻度が低い)
CREATE TABLE user_profiles (
    user_id INT PRIMARY KEY,
    bio TEXT,
    profile_image BLOB,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

アーカイブとライフサイクル管理

1. データアーカイブ戦略

-- 現行データテーブル
CREATE TABLE orders_current (
    id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    total_amount DECIMAL(10,2),
    INDEX idx_order_date (order_date)
);

-- アーカイブテーブル
CREATE TABLE orders_archive (
    id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    total_amount DECIMAL(10,2),
    archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. 自動データクリーンアップ

-- 古いログの定期削除
DELETE FROM access_logs 
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
LIMIT 10000;

セキュリティ考慮事項

1. 機密データの暗号化

CREATE TABLE user_credentials (
    user_id INT PRIMARY KEY,
    password_hash VARCHAR(255) NOT NULL,  -- ハッシュ化済み
    salt VARCHAR(32),
    last_login TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

2. アクセス制御

-- 権限ベースアクセス制御
CREATE TABLE permissions (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    resource VARCHAR(100),
    action VARCHAR(50)
);

CREATE TABLE role_permissions (
    role_id INT,
    permission_id INT,
    PRIMARY KEY (role_id, permission_id)
);

3. 監査ログ

CREATE TABLE audit_trail (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    table_name VARCHAR(100),
    operation ENUM('INSERT', 'UPDATE', 'DELETE'),
    old_values JSON,
    new_values JSON,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

パフォーマンス最適化

1. 非正規化の戦略的使用

-- 集計テーブルで読み取り性能向上
CREATE TABLE customer_summary (
    customer_id INT PRIMARY KEY,
    total_orders INT DEFAULT 0,
    total_spent DECIMAL(12,2) DEFAULT 0,
    last_order_date DATE,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

2. マテリアライズドビュー

-- PostgreSQL マテリアライズドビュー
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT 
    DATE_TRUNC('month', order_date) as month,
    COUNT(*) as order_count,
    SUM(total_amount) as total_sales
FROM orders 
WHERE order_date >= '2024-01-01'
GROUP BY DATE_TRUNC('month', order_date);

-- 定期更新
REFRESH MATERIALIZED VIEW monthly_sales;

3. キャッシュテーブル

CREATE TABLE popular_products_cache (
    product_id INT PRIMARY KEY,
    rank_position INT,
    view_count INT,
    cache_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_rank (rank_position)
);

スケーラビリティ設計

1. 読み取り専用レプリカ対応

-- 読み取り専用クエリの分離を意識した設計
CREATE TABLE product_search_index (
    product_id INT PRIMARY KEY,
    search_text TEXT,
    category_path VARCHAR(500),
    FULLTEXT(search_text),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

2. シャーディング対応

-- シャーディングキーを含む設計
CREATE TABLE user_activities (
    id BIGINT AUTO_INCREMENT,
    user_id INT,  -- シャーディングキー
    activity_type VARCHAR(50),
    timestamp TIMESTAMP,
    PRIMARY KEY (id, user_id),  -- シャーディングキーを含む
    INDEX idx_user_timestamp (user_id, timestamp)
);

バックアップとリカバリ

1. バックアップ戦略

-- ポイントインタイムリカバリ用のログテーブル
CREATE TABLE data_changes_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    table_name VARCHAR(100),
    record_id INT,
    change_type ENUM('INSERT', 'UPDATE', 'DELETE'),
    change_data JSON,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. テストデータ管理

-- 本番データからテスト用データ生成
CREATE TABLE customers_test AS
SELECT 
    id,
    CONCAT('test_user_', id) as name,
    CONCAT('test', id, '@example.com') as email,
    created_at
FROM customers 
WHERE created_at >= '2024-01-01'
LIMIT 1000;

実践的な設計パターン

1. 履歴テーブルパターン

CREATE TABLE price_history (
    id INT AUTO_INCREMENT PRIMARY KEY,
    product_id INT,
    price DECIMAL(10,2),
    valid_from TIMESTAMP,
    valid_to TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id),
    INDEX idx_product_date (product_id, valid_from, valid_to)
);

2. 階層データパターン

-- 閉包テーブルでの階層データ管理
CREATE TABLE categories (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE category_closure (
    ancestor_id INT,
    descendant_id INT,
    depth INT,
    PRIMARY KEY (ancestor_id, descendant_id),
    FOREIGN KEY (ancestor_id) REFERENCES categories(id),
    FOREIGN KEY (descendant_id) REFERENCES categories(id)
);

3. タグ付けパターン

CREATE TABLE tags (
    id INT PRIMARY KEY,
    name VARCHAR(50) UNIQUE
);

CREATE TABLE article_tags (
    article_id INT,
    tag_id INT,
    PRIMARY KEY (article_id, tag_id),
    FOREIGN KEY (article_id) REFERENCES articles(id),
    FOREIGN KEY (tag_id) REFERENCES tags(id)
);

よくある設計ミスと対策

1. NULL値の適切な使用

-- NULL許可の判断
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    middle_name VARCHAR(100),  -- 任意項目はNULL許可
    email VARCHAR(255) NOT NULL,
    phone VARCHAR(20),  -- 任意項目
    salary DECIMAL(10,2) NOT NULL DEFAULT 0  -- デフォルト値設定
);

2. 適切なデータ型サイズ

-- 過度に大きなサイズを避ける
CREATE TABLE products (
    id INT PRIMARY KEY,  -- BIGINT は本当に必要な場合のみ
    name VARCHAR(255),   -- TEXT は大量テキスト時のみ
    description TEXT,
    price DECIMAL(8,2)   -- 999,999.99まで対応
);

3. 循環参照の回避

-- 階層構造での自己参照
CREATE TABLE categories (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    parent_id INT,
    level INT NOT NULL DEFAULT 0,  -- 深度制限
    FOREIGN KEY (parent_id) REFERENCES categories(id),
    CHECK (level <= 10)  -- 無限ループ防止
);

まとめ

データベース設計のベストプラクティスは、正規化の原則を理解した上で、パフォーマンス、保守性、拡張性のバランスを取ることが重要です。ビジネス要件の変化に対応できる柔軟性を保ちつつ、データの整合性を確保する設計を心がけましょう。

継続的な監視と改善により、長期的に安定したデータベースシステムを維持することができます。設計段階での慎重な検討が、将来の運用コストと開発効率に大きく影響することを忘れずに、適切な設計判断を行ってください。

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■AI時代の第一歩!「AI駆動開発コース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<月1開催>放送作家による映像ディレクター養成講座

<オンライン無料>ゼロから始めるPython爆速講座