データベース設計で陥りがちな7つの罠
データベース設計のミスは、後から修正するコストが極めて高くなります。弊社の経験では、設計ミスの修正に平均3.2ヶ月、コストは当初見積もりの8倍かかったケースもあります。
本記事では、実際のプロジェクトで頻発する7つの失敗パターンと、それぞれの対策を具体的に解説します。
罠1: 正規化不足によるデータ重複
問題: 同じ情報が複数の場所に保存され、更新時に不整合が発生する。
• データ更新時に全ての場所を更新する必要がある
• 更新漏れでデータ不整合が発生
• ストレージ容量の無駄
• クエリが複雑化
❌ 悪い例(非正規化)
orders テーブル
| order_id | customer_name | customer_email | product_name | price | |----------|---------------|-------------------|--------------|-------| | 1 | 山田太郎 | [email protected]| ノートPC | 98000 | | 2 | 山田太郎 | [email protected]| マウス | 2000 | 問題: 顧客情報が重複。メールアドレス変更時に全行更新が必要。
✅ 良い例(正規化)
customers テーブル
| customer_id | name | email | |-------------|-----------|---------------------| | 1 | 山田太郎 | [email protected] |
orders テーブル
| order_id | customer_id | product_name | price | |----------|-------------|--------------|-------| | 1 | 1 | ノートPC | 98000 | | 2 | 1 | マウス | 2000 |
✅ 顧客情報は1箇所のみ。メールアドレス変更は1行だけ更新。
対策: 第3正規形(3NF)以上を目指す。ただし、パフォーマンスが必要な場合は意図的に非正規化することもある(その場合はトレードオフを明確に)。
罠2: インデックスの不適切な設計
問題: インデックスなし、または過剰なインデックスでパフォーマンスが悪化。
• インデックスなし: クエリが遅い(全件スキャン)
• インデックス過剰: 書き込みが遅くなる
• 使われないインデックス: ストレージの無駄
インデックスが必要なケース:
- • WHERE句で頻繁に使われるカラム
- • JOIN条件に使われるカラム
- • ORDER BY句で使われるカラム
- • 外部キー制約のカラム
- • ユニーク制約が必要なカラム
適切なインデックス例:
-- よく検索されるカラム CREATE INDEX idx_users_email ON users(email); -- 複合インデックス(複数カラムでのWHERE) CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date); -- カバリングインデックス(SELECT対象も含む) CREATE INDEX idx_users_active_email_name ON users(is_active, email, name);
やりがちな失敗:
-- 全てのカラムにインデックスを貼る(過剰) CREATE INDEX idx_users_created_at ON users(created_at); CREATE INDEX idx_users_updated_at ON users(updated_at); CREATE INDEX idx_users_last_login ON users(last_login); -- ↑ほとんど使われないインデックス → 書き込みが遅くなる
対策: クエリログを分析し、本当に必要なインデックスだけを作成。不要なインデックスは定期的に削除。EXPLAINで実行計画を確認する習慣をつける。
罠3: NULL値の扱いが曖昧
問題: NULLの意味が不明確で、クエリが複雑化し、バグの温床になる。
• NULLは「不明」なのか「該当なし」なのか「未入力」なのか
• NULL判定(IS NULL / IS NOT NULL)を忘れる
• COUNT()などの集計関数でNULLが除外される
悪い例:
CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100), -- NULLとは?未登録? email VARCHAR(100), -- NULLとは?持ってない?未確認? age INT -- NULLとは?未入力?秘密? );
良い例:
CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, -- 必須 email VARCHAR(100) NOT NULL UNIQUE, -- 必須かつユニーク email_verified BOOLEAN DEFAULT FALSE, -- 確認済みフラグ age INT DEFAULT NULL, -- 任意(未入力はNULL) birth_date DATE DEFAULT NULL -- 任意(非公開はNULL) ); -- NULLの意味を明確に: -- age=NULL: 年齢を入力していない -- birth_date=NULL: 生年月日を公開していない
対策: 可能な限り NOT NULL制約を使う。NULLを使う場合は、その意味をコメントやドキュメントで明確にする。デフォルト値の設定も検討。
罠4: 文字コードと照合順序の不一致
問題: テーブルごとに文字コードが違うと、JOINやUNIONでエラーが発生。
• 異なる文字コードのカラムでJOINできない
• 照合順序の違いでソート結果が異なる
• 絵文字が保存できない(utf8mb3の場合)
推奨設定(MySQL):
-- データベース作成時に指定 CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 既存DBの変更 ALTER DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- テーブル作成時 CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
注意: utf8(utf8mb3)は絵文字が保存できない。必ずutf8mb4を使う。PostgreSQLはデフォルトでUTF8なので問題なし。
罠5: トランザクション管理の不備
問題: トランザクションを適切に使わず、データ不整合が発生。
• 複数テーブルの更新が途中で失敗し、データが中途半端な状態に
• 同時実行制御ができず、競合が発生
• デッドロックが頻発
悪い例(トランザクションなし):
// 銀行の送金処理 UPDATE accounts SET balance = balance - 10000 WHERE id = 1; // ← ここでエラーが発生したら? UPDATE accounts SET balance = balance + 10000 WHERE id = 2; // → 10000円が消失する
良い例(トランザクション使用):
BEGIN TRANSACTION; UPDATE accounts SET balance = balance - 10000 WHERE id = 1; UPDATE accounts SET balance = balance + 10000 WHERE id = 2; COMMIT; -- エラーが発生したら自動的にROLLBACKされる
対策: 複数テーブルの更新は必ずトランザクションで囲む。分離レベルも適切に設定(READ COMMITTED、REPEATABLE READ等)。
罠6: スケーラビリティを考慮しない設計
問題: データ量増加を想定せず、後から性能問題が発生。
• 1テーブルに数千万行でクエリが極端に遅くなる
• パーティショニングできない構造
• 水平分割(シャーディング)が困難
対策一覧:
1. パーティショニング
日付や範囲で物理的にテーブルを分割し、クエリ対象を絞る。
-- 日付でパーティション CREATE TABLE logs ( id BIGINT, created_at DATE, message TEXT ) PARTITION BY RANGE (YEAR(created_at)) ( PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN (2025) );
2. アーカイブ戦略
古いデータは別テーブルやストレージに移動し、本番テーブルを小さく保つ。
3. 読み取りレプリカ
書き込みはマスター、読み取りはレプリカで負荷分散。
罠7: マイグレーション戦略の欠如
問題: スキーマ変更の履歴管理がなく、本番環境での変更が危険。
• どのバージョンのスキーマか不明
• ロールバックできない
• 開発・本番で構造が異なる
推奨ツール:
- • Flyway: Java系、バージョン管理
- • Liquibase: XML/YAML/JSONで定義
- • Rails Migrations: Ruby on Rails標準
- • Alembic: Python/SQLAlchemy
- • Prisma Migrate: Node.js/TypeScript
まとめ
データベース設計は後から直すのが極めて困難です。最初から正しい設計を心がけることで、将来の保守コストを大幅に削減できます。
特に、正規化、インデックス、NULL値の扱い、トランザクション管理は基本中の基本。これらを疎かにすると、必ず後悔することになります。
弊社では、これらの設計原則を適用することで、データベース起因のトラブルを90%以上削減し、スケーラビリティも確保しています。