システム開発

データベース設計で陥りがちな7つの罠

2025-11-21
17分

データベース設計で陥りがちな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%以上削減し、スケーラビリティも確保しています。

この記事をシェア:

おすすめの記事

株式会社Apple Seed - システム開発・AI開発