データベース設計のアンチパターン:避けるべき7つの落とし穴

データベース

データベース設計は、アプリケーションの性能と保守性を左右する重要な要素です。しかし、開発の現場では様々な理由から「アンチパターン」と呼ばれる問題のある設計が生まれてしまうことがあります。この記事では、よく見かけるデータベースのアンチパターンと、その解決策について解説します。

1. ジェイウォーク(Jaywalking):カンマ区切りリストの格納

最も頻繁に見かけるアンチパターンの一つが、複数の値をカンマ区切りで1つのカラムに格納するパターンです。

アンチパターン

articlesテーブル

idtitletag_ids
1Rails入門1,3,5
2Vue.js実践2,3
3DB設計1,4,5,12

tag_idsにカンマ区切りで複数のタグIDを格納しています。

なぜダメか

  • 特定のタグを持つ記事を検索する際にLIKE '%,3,%'のような非効率なクエリが必要になり、インデックスが効かない
  • 参照整合性を保証できないため、存在しないタグIDが紛れ込む可能性がある
  • 集計処理が複雑になり、タグごとの記事数を数えるような単純な操作でさえ困難になる
  • VARCHAR(255)などの文字数制限により、格納できるIDの数に暗黙の上限が生じる

解決策:交差テーブルの作成

articlesテーブル

idtitle
1Rails入門
2Vue.js実践
3DB設計

article_tagsテーブル(交差テーブル)

article_idtag_id
11
13
15
22
23
31
34
35
312
-- tag_id = 3 を持つ記事の検索が簡単に
SELECT * FROM articles a
INNER JOIN article_tags at ON a.id = at.article_id
WHERE at.tag_id = 3;

2. EAV(Entity-Attribute-Value):柔軟性という名の罠

「将来どんな属性が追加されるかわからないから」という理由で採用されがちなのがEAVパターンです。

アンチパターン

product_attributesテーブル

idproduct_idattr_nameattr_value
11colorred
21sizeM
31price1980
42colorblue
52weight500g

なぜダメか

  • データ型の制約が効かないため、priceに文字列が入っても検出できない
  • 必須属性の強制が不可能(NOT NULL制約が使えない)
  • 単純なSELECTでも複雑なJOINやPIVOTが必要になる
  • 外部キー制約による参照整合性を強制できない

解決策

まず本当にスキーマレスが必要か再検討することが重要です。多くの場合、属性はある程度予測可能です。

productsテーブル

idnamepricecolorsizeattributes
1Tシャツ1980redM{“material”: “cotton”}
2バッグ3500blueNULL{“weight”: “500g”}

固定的な属性は通常のカラムで、動的な属性はJSON型カラムで管理します。

3. マジックナンバーとしてのNULL

NULLを「未設定」以外の意味で使用するパターンも危険です。

アンチパターン

subscriptionsテーブル

iduser_idstart_dateend_date
11012024-01-012024-12-31
21022024-03-01NULL
31032024-06-01NULL

end_dateがNULL = 「現在も有効」という暗黙のルールになっています。

なぜダメか

  • NULLは「値が存在しない」ことを表すべきで、特定のビジネスロジックを表現するために使うべきではない
  • 本当に値が未設定なのか、それとも「有効」を意味するのか区別できない
  • NULLを含む演算は結果がNULLになるため、集計時に予期せぬ結果を招く

解決策:明示的なステータスカラム

subscriptionsテーブル

iduser_idstart_dateend_dateis_active
11012024-01-012024-12-31false
21022024-03-01NULLtrue
31032024-06-01NULLtrue

ビジネスロジックは明示的なカラムで表現します。

4. インデックスショットガン:とりあえずインデックス

パフォーマンス問題が発生すると、「とりあえずインデックスを追加しよう」という対応がよく見られます。

アンチパターン

usersテーブルのインデックス一覧

Key_nameColumn_name
PRIMARYid
idx_emailemail
idx_namename
idx_email_nameemail, name
idx_name_emailname, email
idx_createdcreated_at
idx_email_createdemail, created_at

なぜダメか

  • 不要なインデックスはINSERT/UPDATE/DELETEの性能を低下させる
  • ストレージを消費する
  • idx_emailidx_email_nameは重複している(後者で前者の用途をカバーできる)
  • 複合インデックスの順序を誤ると、意図したクエリで使用されない
    • WHERE name = ? AND email = ?ではidx_email_nameは効率的に使われない

解決策

  • EXPLAINを使って実際のクエリプランを確認する
  • 本当に必要なインデックスだけを作成する
  • 定期的に未使用インデックスを棚卸しする
-- クエリの実行計画を確認
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

5. ポリモーフィック関連:型の曖昧な外部キー

複数のテーブルへの参照を1つのカラムで表現しようとするパターンです。

アンチパターン

commentsテーブル

idbodycommentable_typecommentable_id
1良い記事ですねArticle1
2素敵な写真!Photo5
3面白い動画Video3
4参考になりましたArticle2

commentable_typeで参照先テーブルを、commentable_idでそのIDを指定しています。

なぜダメか

  • 外部キー制約が設定できず、参照整合性を保証できない
  • JOINが複雑になり、クエリの可読性が著しく低下する
  • 存在しないレコードへの参照が混入しても検出できない

解決策1:テーブル分割

article_commentsテーブル

idarticle_idbody
11良い記事ですね
22参考になりました

photo_commentsテーブル

idphoto_idbody
15素敵な写真!

解決策2:共通親テーブル

commentablesテーブル

idtype
1Article
2Photo

commentsテーブル

idcommentable_idbody
11良い記事ですね
22素敵な写真!

commentable_idに外部キー制約を設定できます。

6. 神テーブル(God Table)

「関連しそうなデータは全部1つのテーブルに」という発想で生まれる巨大テーブルです。

アンチパターン

usersテーブル

カラム名用途
id主キー
name基本情報
email基本情報
encrypted_password認証情報
phoneプロフィール
addressプロフィール
notification_email設定
notification_push設定
theme設定
plan_type課金情報
billing_address課金情報
credit_card_last4課金情報
login_count統計
last_login_at統計
さらに30カラム以上

なぜダメか

  • NULLだらけのスパースなデータになりがち
  • テーブルの責務が不明確になる
  • 変更の影響範囲が広くなり、ALTER TABLEが恐怖になる
  • 特定の機能だけ使いたい場合でも全カラムをロードしてしまう

解決策:責務ごとにテーブル分割

usersテーブル(認証に必要な最小限)

idnameemailencrypted_password

user_profilesテーブル

user_idphoneaddress

user_settingsテーブル

user_idnotification_emailnotification_pushtheme

user_billingsテーブル

user_idplan_typebilling_addresscredit_card_last4

7. ソフトデリート依存症

「データは消さない、フラグを立てるだけ」という方針の過度な適用です。

アンチパターン

ordersテーブル

iduser_idtotalstatusdeleted_at
11011500completedNULL
21022300pendingNULL
3101800completed2024-01-15 10:30:00

全テーブルにdeleted_atがあり、全クエリにWHERE deleted_at IS NULLが必要になります。

なぜダメか

  • クエリにdeleted_at条件を書き忘れるリスクが常にある
  • インデックスの効率が低下する
  • データ量が際限なく増加する
  • UNIQUE制約との相性が悪い(削除済みレコードとの重複問題)

解決策

まず本当にソフトデリートが必要か再検討します。

監査目的ならアーカイブテーブルへの移動を検討:

ordersテーブル(現役データのみ)

iduser_idtotalstatus
11011500completed
21022300pending

archived_ordersテーブル

iduser_idtotalstatusarchived_at
3101800completed2024-01-15 10:30:00

どうしてもソフトデリートが必要な場合は、ビューやデフォルトスコープで条件漏れを防ぎます。

まとめ

データベースのアンチパターンは、短期的には問題なく動作することが多いため、発見が遅れがちです。しかし、データ量の増加やビジネス要件の変化に伴い、大きな技術的負債となって返ってきます。

設計時に意識すべきポイント:

  • 正規化の原則を理解し適切に適用する
  • 外部キー制約を活用して参照整合性を保証する
  • 将来の拡張性とのバランスを考える
  • EXPLAINを習慣化しクエリの実行計画を確認する

「動くからいい」ではなく、「なぜこの設計なのか」を説明できる状態を目指しましょう。

参考文献

Bill Karwin 著『SQLアンチパターン』(オライリージャパン、2013年)

原著は『SQL Antipatterns: Avoiding the Pitfalls of Database Programming』として2010年に出版されました。本記事で取り上げたジェイウォーク、EAV、ポリモーフィック関連などのアンチパターンは、この書籍で体系的に解説されています。

奥野幹也 著『理論から学ぶデータベース実践入門』(技術評論社、2015年)

リレーショナルモデルの理論的背景から実践的な設計手法まで、日本語で体系的に学べる書籍です。

ミック 著『達人に学ぶDB設計 徹底指南書』(翔泳社、2012年)

論理設計から物理設計まで、データベース設計の全体像を学べる入門書です。

コメント

タイトルとURLをコピーしました