データベース設計の原則:正規化から実践的なスキーマ設計まで
データベース設計の基本原則から正規化理論、実際のプロジェクトで使える設計パターンまで詳しく解説します。
データベース設計の原則
効率的で保守性の高いアプリケーションを構築するために、適切なデータベース設計は不可欠です。基本原則から実践的なテクニックまで学びましょう。
データベース設計の基本概念
エンティティとリレーションシップ
-- ユーザーエンティティ
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- プロフィールエンティティ(1対1関係)
CREATE TABLE user_profiles (
user_id INTEGER PRIMARY KEY,
bio TEXT,
avatar_url VARCHAR(255),
birth_date DATE,
location VARCHAR(100),
website VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 投稿エンティティ(1対多関係)
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'draft',
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- タグエンティティ(多対多関係)
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 投稿とタグの中間テーブル
CREATE TABLE post_tags (
post_id INTEGER,
tag_id INTEGER,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
正規化理論
第 1 正規形(1NF)
-- 非正規化テーブル(悪い例)
CREATE TABLE orders_bad (
id SERIAL PRIMARY KEY,
customer_name VARCHAR(100),
products VARCHAR(500), -- 'Product1,Product2,Product3'
quantities VARCHAR(100) -- '2,1,3'
);
-- 第1正規形(良い例)
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_name VARCHAR(100),
order_date DATE DEFAULT CURRENT_DATE
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER,
product_name VARCHAR(100),
quantity INTEGER,
price DECIMAL(10,2),
FOREIGN KEY (order_id) REFERENCES orders(id)
);
第 2 正規形(2NF)
-- 第1正規形だが第2正規形違反(悪い例)
CREATE TABLE order_details_bad (
order_id INTEGER,
product_id INTEGER,
product_name VARCHAR(100), -- product_idに部分従属
product_category VARCHAR(50), -- product_idに部分従属
quantity INTEGER,
PRIMARY KEY (order_id, product_id)
);
-- 第2正規形(良い例)
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
category VARCHAR(50),
price DECIMAL(10,2),
description TEXT
);
CREATE TABLE order_details (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
unit_price DECIMAL(10,2),
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
第 3 正規形(3NF)
-- 第2正規形だが第3正規形違反(悪い例)
CREATE TABLE employees_bad (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
department_id INTEGER,
department_name VARCHAR(100), -- department_idに推移従属
department_location VARCHAR(100) -- department_idに推移従属
);
-- 第3正規形(良い例)
CREATE TABLE departments (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
location VARCHAR(100),
budget DECIMAL(12,2)
);
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
department_id INTEGER,
position VARCHAR(50),
salary DECIMAL(10,2),
hire_date DATE,
FOREIGN KEY (department_id) REFERENCES departments(id)
);
インデックス設計
基本的なインデックス
-- 主キーインデックス(自動作成)
-- PRIMARY KEY制約により自動的に作成される
-- ユニークインデックス
CREATE UNIQUE INDEX idx_users_username ON users(username);
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- 単一カラムインデックス
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published_at ON posts(published_at);
-- 複合インデックス
CREATE INDEX idx_posts_user_status ON posts(user_id, status);
CREATE INDEX idx_posts_status_published ON posts(status, published_at);
-- 部分インデックス
CREATE INDEX idx_posts_published ON posts(published_at)
WHERE status = 'published';
-- 式インデックス
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
インデックス使用例とクエリ最適化
-- インデックスが効果的なクエリ
EXPLAIN ANALYZE
SELECT * FROM posts
WHERE user_id = 123 AND status = 'published'
ORDER BY published_at DESC;
-- インデックスが効果的でないクエリ(改善が必要)
EXPLAIN ANALYZE
SELECT * FROM posts
WHERE content LIKE '%keyword%'; -- 前方一致でない
-- 改善版:全文検索インデックス
CREATE INDEX idx_posts_content_fulltext ON posts
USING GIN(to_tsvector('japanese', content));
SELECT * FROM posts
WHERE to_tsvector('japanese', content) @@ to_tsquery('keyword');
実践的な設計パターン
ブログシステムの完全なスキーマ
-- ユーザー管理
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'user',
is_active BOOLEAN DEFAULT true,
email_verified_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- カテゴリ(階層構造)
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
parent_id INTEGER,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
FOREIGN KEY (parent_id) REFERENCES categories(id)
);
-- 投稿
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
category_id INTEGER,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
excerpt TEXT,
content TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'draft',
featured_image VARCHAR(255),
meta_title VARCHAR(255),
meta_description TEXT,
view_count INTEGER DEFAULT 0,
like_count INTEGER DEFAULT 0,
comment_count INTEGER DEFAULT 0,
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
);
-- コメント(階層構造)
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL,
user_id INTEGER,
parent_id INTEGER,
author_name VARCHAR(100),
author_email VARCHAR(100),
content TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
);
-- タグ
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
color VARCHAR(7), -- HEXカラーコード
post_count INTEGER DEFAULT 0
);
-- 投稿とタグの関連
CREATE TABLE post_tags (
post_id INTEGER,
tag_id INTEGER,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
-- いいね機能
CREATE TABLE post_likes (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(post_id, user_id),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
E-commerce システムの設計例
-- 商品カテゴリ
CREATE TABLE product_categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
parent_id INTEGER,
description TEXT,
image_url VARCHAR(255),
is_active BOOLEAN DEFAULT true,
sort_order INTEGER DEFAULT 0,
FOREIGN KEY (parent_id) REFERENCES product_categories(id)
);
-- 商品
CREATE TABLE products (
id SERIAL PRIMARY KEY,
category_id INTEGER,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
short_description TEXT,
sku VARCHAR(50) UNIQUE NOT NULL,
price DECIMAL(10,2) NOT NULL,
sale_price DECIMAL(10,2),
stock_quantity INTEGER DEFAULT 0,
weight DECIMAL(8,2),
dimensions VARCHAR(50),
is_active BOOLEAN DEFAULT true,
is_featured BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES product_categories(id)
);
-- 商品バリエーション
CREATE TABLE product_variants (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL,
sku VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100),
price DECIMAL(10,2),
stock_quantity INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
-- 商品属性(サイズ、色など)
CREATE TABLE product_attributes (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
type VARCHAR(20) DEFAULT 'text' -- text, select, color, etc.
);
CREATE TABLE product_attribute_values (
id SERIAL PRIMARY KEY,
attribute_id INTEGER NOT NULL,
value VARCHAR(100) NOT NULL,
FOREIGN KEY (attribute_id) REFERENCES product_attributes(id)
);
CREATE TABLE product_variant_attributes (
variant_id INTEGER,
attribute_value_id INTEGER,
PRIMARY KEY (variant_id, attribute_value_id),
FOREIGN KEY (variant_id) REFERENCES product_variants(id) ON DELETE CASCADE,
FOREIGN KEY (attribute_value_id) REFERENCES product_attribute_values(id)
);
-- 注文管理
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER,
order_number VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
subtotal DECIMAL(10,2) NOT NULL,
tax_amount DECIMAL(10,2) DEFAULT 0,
shipping_amount DECIMAL(10,2) DEFAULT 0,
total_amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'JPY',
payment_status VARCHAR(20) DEFAULT 'pending',
payment_method VARCHAR(50),
shipping_address JSONB,
billing_address JSONB,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
variant_id INTEGER,
quantity INTEGER NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
total_price DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id),
FOREIGN KEY (variant_id) REFERENCES product_variants(id)
);
パフォーマンス最適化
適切なデータ型選択
-- 効率的なデータ型の選択
CREATE TABLE performance_example (
-- 整数型:必要最小限のサイズを選択
small_number SMALLINT, -- -32,768 to 32,767
medium_number INTEGER, -- -2,147,483,648 to 2,147,483,647
large_number BIGINT, -- -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
-- 文字列型:適切な長さを指定
status CHAR(1), -- 固定長(Y/N, M/F など)
code VARCHAR(10), -- 可変長(短い文字列)
description TEXT, -- 長い文字列
-- 真偽値
is_active BOOLEAN, -- true/false
-- 日時
created_at TIMESTAMP, -- タイムゾーン情報なし
updated_at TIMESTAMPTZ, -- タイムゾーン情報あり
birth_date DATE, -- 日付のみ
-- 数値
price DECIMAL(10,2), -- 精密な小数点計算が必要
ratio REAL, -- 近似値で十分
-- JSON
metadata JSONB -- JSONより高性能
);
パーティショニング
-- 日付によるパーティショニング
CREATE TABLE logs (
id SERIAL,
created_at DATE NOT NULL,
user_id INTEGER,
action VARCHAR(50),
data JSONB
) PARTITION BY RANGE (created_at);
-- パーティション作成
CREATE TABLE logs_2024_01 PARTITION OF logs
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE logs_2024_02 PARTITION OF logs
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE logs_2024_03 PARTITION OF logs
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
-- 各パーティションにインデックス作成
CREATE INDEX idx_logs_2024_01_user_id ON logs_2024_01(user_id);
CREATE INDEX idx_logs_2024_02_user_id ON logs_2024_02(user_id);
CREATE INDEX idx_logs_2024_03_user_id ON logs_2024_03(user_id);
マテリアライズドビュー
-- 集計データのマテリアライズドビュー
CREATE MATERIALIZED VIEW monthly_sales_summary AS
SELECT
DATE_TRUNC('month', created_at) as month,
COUNT(*) as order_count,
SUM(total_amount) as total_sales,
AVG(total_amount) as average_order_value,
COUNT(DISTINCT user_id) as unique_customers
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;
-- インデックス作成
CREATE UNIQUE INDEX idx_monthly_sales_month ON monthly_sales_summary(month);
-- 定期的な更新(cron jobなどで実行)
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales_summary;
セキュリティ考慮事項
アクセス制御
-- ユーザーとロールの作成
CREATE ROLE blog_read_only;
CREATE ROLE blog_write;
CREATE ROLE blog_admin;
-- 権限の付与
GRANT SELECT ON ALL TABLES IN SCHEMA public TO blog_read_only;
GRANT SELECT, INSERT, UPDATE ON posts, comments TO blog_write;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO blog_admin;
-- ユーザー作成と権限割り当て
CREATE USER blog_app WITH PASSWORD 'secure_password';
GRANT blog_write TO blog_app;
-- 行レベルセキュリティ
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_posts_policy ON posts
FOR ALL TO blog_app
USING (user_id = current_setting('app.current_user_id')::INTEGER);
データの暗号化
-- 機密データの暗号化
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE sensitive_data (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
encrypted_ssn TEXT, -- 暗号化された社会保障番号
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- データの挿入時に暗号化
INSERT INTO sensitive_data (user_id, encrypted_ssn)
VALUES (1, crypt('123-45-6789', gen_salt('bf', 8)));
-- データの検索
SELECT id, user_id
FROM sensitive_data
WHERE encrypted_ssn = crypt('123-45-6789', encrypted_ssn);
バックアップと復旧戦略
バックアップスクリプト例
#!/bin/bash
# database_backup.sh
DB_NAME="myapp_production"
BACKUP_DIR="/var/backups/postgresql"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql"
# ディレクトリが存在しない場合は作成
mkdir -p $BACKUP_DIR
# データベースダンプ
pg_dump $DB_NAME > $BACKUP_FILE
# 圧縮
gzip $BACKUP_FILE
# 7日以上古いバックアップを削除
find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete
echo "Backup completed: ${BACKUP_FILE}.gz"
復旧手順
-- データベースの復旧
-- 1. 新しいデータベース作成
CREATE DATABASE myapp_restored;
-- 2. バックアップからの復元
-- psql myapp_restored < backup_file.sql
-- 3. データ整合性チェック
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM posts;
SELECT COUNT(*) FROM comments;
-- 4. インデックス再構築(必要に応じて)
REINDEX DATABASE myapp_restored;
監視とメンテナンス
パフォーマンス監視クエリ
-- 実行時間が長いクエリの特定
SELECT
query,
calls,
total_time,
mean_time,
max_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;
-- テーブルサイズの確認
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
-- インデックス使用状況
SELECT
indexrelname,
idx_tup_read,
idx_tup_fetch,
idx_scan
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
定期メンテナンス
-- 統計情報の更新
ANALYZE;
-- 不要な領域の回収
VACUUM;
-- 完全なバキューム(テーブルロックが発生)
VACUUM FULL;
-- 自動バキュームの設定確認
SELECT name, setting
FROM pg_settings
WHERE name LIKE 'autovacuum%';
まとめ
効果的なデータベース設計は、アプリケーションの性能と保守性に大きく影響します。正規化理論を理解し、適切なインデックス設計を行い、セキュリティとパフォーマンスを考慮した設計を心がけましょう。
継続的な監視とメンテナンスにより、データベースの健全性を保ち、アプリケーションの成長に対応できる柔軟な設計を目指しましょう。
最後まで読んでいただきありがとうございました!てばさん(@basabasa8770)でした!