/note/tech

なぜ気軽にテーブルにカラムを足してはいけないのか

要約:

■ 1. 記事の目的

  • 問題提起: データベース(RDB)の設計で深く考えずにテーブルにカラムを追加してしまうことはないか
  • 心理的ハードル: テーブルの追加よりもアプリケーション側での変更が少ないので、心理的ハードルが低い
  • 警告: カラム追加を続けると取り返しのつかないことになるかもしれない

■ 2. 3つの主要な問題

  • データベース設計がちゃんとできていないことの兆候である
  • 変更のコストが大きい
  • インデックスの設計が難しくなる

■ 3. Database Smell(データベースの設計上の悪い兆候)

  • 定義: カラムが多すぎるテーブルはDatabase Smellの1つ
  • カラムを気軽に増やす場合の問題:
    • 正規化ができていない
    • 複数エンティティを混在させてしまう

■ 4. 正規化ができていない:マルチカラムアトリビュート

  • 呼び名: SQLアンチパターンでの呼び名で、列持ちテーブルとも呼ばれる
  • 具体例: 連絡先テーブルでphone1、phone2、phone3と電話番号カラムを追加していく設計
  • 問題点:
    • 特定の電話番号を検索するには全てのカラムを検索する必要がある
    • 一意性の保証ができない
    • 更新時にどのカラムを更新するかのロジックが必要になる
  • 正しい設計: 電話番号を別テーブルに分割し、1対Nの関係を持たせる

■ 5. 正規化の重要性

  • 基本中の基本: 正規化はデータベース設計の基本中の基本
  • 参考文献: 「達人に学ぶDB設計徹底指南書」第一版の紙面のほとんどを使って正規化について説明されていた
  • パフォーマンスとの関係: パフォーマンスの問題などで正規化を崩すことが必要な時もあるが、最初の設計としては正規化するところから始めることを推奨

■ 6. 複数エンティティの混在

  • 問題の発生: 複数のエンティティ(データモデル内で個別に識別可能な要素やオブジェクトを表す概念)が1つのテーブルに混在してしまう
  • 具体例(注文テーブル):
    • 最初は注文エンティティのみを表現
    • 後から発送日(shipped_on)カラムを追加
    • 結果として注文エンティティと発送エンティティの一部が混在
  • 正しい設計: 発送は別のテーブルとして切り出すのが適切

■ 7. よくある失敗パターン

  • 安直な追加: 日時(xxx_at / xxx_on)やフラグ(xxx_flag)やステータス(xxx_status)のカラムを追加すること
  • 対策: これらを追加したくなったときには他のエンティティではないかと疑う

■ 8. 変更のコストの大きさ

  • 基本認識: アプリケーションのリファクタリングと比べて、データベースのリファクタリングはコスト(時間・工数・リスク)が大きい
  • 考慮事項:
    • 影響範囲がどの程度あるか
    • 既存データがどの程度あるか
    • ダウンタイムがどの程度許容されるか
  • 覚悟の必要性: データベースリファクタリングは影響範囲も広く時間がかかるため、やり切るぞという覚悟が必要(そーだいさんのブログより)

■ 9. ロックとダウンタイムの問題

  • ALTER TABLEのロック: 多くのケースでロックを取る
  • レコード数が多い時の問題: 実行時間が長くなり、数時間そのテーブルに対する書き込みができなくなる場合もある
  • 新テーブルへのINSERT: レコード数が多い場合、実行時間が長くかかったり、データベースのCPUを圧迫する可能性がある
  • 事前準備: テスト環境でデータ移行のリハーサルを行い、実行時間やデータベースの負荷を確認しておくことが重要

■ 10. Dual Write期間の問題

  • 定義: データ移行が大規模でリスクが大きい場合、アプリケーション側で新旧両方のテーブルに書き込むDual Writeを行うプラクティス
  • メリット: アプリケーションのロールバックが可能になり、データ移行のリスクを減らせる
  • デメリット:
    • アプリケーション内部での複雑性が増す
    • コードの変更にかかるコストが増大
    • リリースも複数段階に分けて行う必要があり、スケジュール管理も複雑になる

■ 11. 時間経過による移行コストの増大

  • 移行自体のコスト:
    • レコード数が大きくなるとロックを取る時間やリソースの逼迫に大きく影響
    • 場合によっては8時間以上かかることもある
    • 「一晩のメンテだけで済むと思ってたら翌営業日の朝になっても終らない」という事態もありえる
  • 警告: 本番環境でデータ移行を実施するときに「postgresql alter table 終わらない」というGoogle検索をしないようにすべき

■ 12. 影響範囲を調べるコスト

  • カラム依存の調査: カラムを別テーブルに移動させる際に、アプリケーションの影響範囲を調査する必要がある
  • テーブル依存の影響: カラムに依存していなくてもテーブルへの依存があると、カラムに依存がないかどうかを確認する必要が出てくる
  • 時間経過の影響: 時間が経つほどテーブルに対する依存が増えるため、カラムの依存調査にかかる手間が増える

■ 13. データの予期せぬ値の問題

  • Git管理の限界: データベースに入っているデータそのものはアプリケーションコードのようにGitを使って差分管理できない
  • 予期せぬ値: 全く予期せぬ値が入っていることもありえる
  • 保証の限界: 保証してくれるのはデータベースのスキーマ定義のみ
  • バグの混入: 今のアプリケーションコードでは入りえない値が過去の期間だけ存在したバグによって混入することもありえる

■ 14. システムの使われ方の変化

  • 想定外の使用: 時間の推移とユーザーの増加によってシステムの使われ方が変わりえる
  • 予期せぬデータ: もともと想定していたのとは違う使われ方をしていたり、想定しない値が入っている可能性がある
  • 検証の必要性: 昔からある膨大なレコードに対しては事前に入念な検証が必要

■ 15. インデックス設計の困難化

  • 基本原則: 一般的にカラムが少ないほどインデックス設計は楽で、カラム数が増えるほど設計は難しくなりやすい
  • インデックスの制約: インデックスは1つのテーブルに対してスキャンする際には1つしか使われない
  • 複合インデックス: 複数のカラムに対してインデックスを使った検索を行いたい場合は複合インデックスを使う

■ 16. 複合インデックスの制約

  • 左方一致の原則: 一般的に複合インデックスは左方一致する条件でしか利用されない(左から順番にしか使われない)
  • 具体例(書籍テーブル):
    • CREATE INDEX books_index ON books (category_id, publisher_id)
    • category_idとpublisher_idでの検索:インデックス利用
    • category_idのみでの検索:インデックス利用
    • publisher_idのみでの検索:インデックス利用されない(左側のカラムが使われていないため)

■ 17. インデックスショットガンの問題

  • 定義: むやみやたらにインデックスを増やすと更新時のパフォーマンスが大きく劣化する問題(SQLアンチパターンで説明)
  • カラム追加の影響: 気軽にカラムを増やすと後でパフォーマンスチューニングをする際にも足枷になりやすい

■ 18. 「慣れるより習う」の重要性

  • 問題発見の遅れ: データベース設計の失敗は時間が経ってから気づくことが多い
  • 同じ失敗の繰り返し: 自分で失敗した内容から学ばずに、異動や転職などで新しい環境に行って同じ失敗を繰り返してしまうこともある
  • 他者から学ぶ: 他の人が失敗した内容を見て学ぶことはできる
  • 理想状態の困難: 問題が大きくなってしまった後では理想の状態を描くことが難しい

■ 19. 問題の遅延発生

  • そーだいさんの言葉: 「DBの問題は忘れた頃にやってくる」
  • 学習資料の重要性: 学習のためにいい本や資料はたくさんある

■ 20. まとめ

  • カラム追加の基本性: データベースの操作の中でも基本的な操作だが、さまざまな問題を起こしうる
  • 適切な場合: カラム追加が適切な場合ももちろんあるが、適切なデータモデリング・テーブル設計が行われた上で初めて判断できる
  • 経験と学習: 適切にデータベース設計ができるようになるまでは経験と学習が必要