正規化のすすめ – 設計編(その3 – 主キー設計)

 今回は設計編 その1で挙げた「正規化してもうまくいかない」という問題について、その理由のひとつとして主キー設計について取り上げたいと思います。

 主キーは単なる検索用のインデックスではなく、極めて重要な意味を持っています。
 この点を理解せずに主キーを定義すると、設計が行き詰まってしまいます。

 主キーについての一般的な知識はご理解いただいているものとして、主キー設計で見落としがちな観点について解説します。

主キーはなぜ更新できないのか

 主キー制約には以下の規則があり、逸脱することはできません。

  1. 主キーの値は一意であること(UNIQUE制約)
  2. 主キーの値はNULLでないこと(NOT NULL制約)
  3. 主キーの値は更新できない

 1.と2.の意味は分かると思いますが、3.の意味が分かりづらいことで、
   「主キーは面倒なので定義したくない」
   「主キーが更新できないのはRDBの欠陥だ」
といった意見を見かけます。

 しかしこの規則にはきちんとした理由があり、RDBとして必要不可欠な定義です。


 3.の規則は、データの一貫性を保証するための定義です。

 DBのレコードは、無秩序なデータの集合ではありません。それは一定の意味を持つデータのまとまりであり、例えば人・商品・受注といった意味を持つ単位とその属性を表します。
 そして主キーはレコードを一意に特定する情報であり、そのデータそのものを表します。

 例えば個人を定義するマスタでは、レコードはAさん・Bさんといった特定の人を表す情報であり、Aさんの主キーの値は、Aさん自身を端的に表す情報です。
 もし主キーが更新可能であると、最初はAさんを表していたデータが、後から別人に置き換わることを意味します。するとAさんが購入した商品について、Aさんは存在しなかったことになり、別人に対して請求が発生するといった矛盾が発生します。

 これはデータの一貫性が失われた状態です。
 現実の世界では、Aさんの表面的な属性である氏名や住所が変わることはあっても、Aさん自体が全く別人に置き変わることは起こりません。商品や受注といった概念でも同様です。

 このようなデータの本質を変えてしまう更新は矛盾を引き起こすため、設計上あってはなりません。別の本質のデータは、別のレコードと主キーで表されるべきです。

 したがって、いかなる場合でもデータの本質が不変であることを保証するために、主キーは更新不可と定義されています。 

なぜ主キーが必要か

 主キーを設計する上で、主キーの目的を理解することが重要です。

 主キーの目的を「検索用の情報を定義する」と捉えるだけでは不十分です。
 より重要な目的は、以下を定義することにあります。

  1. レコードの意味を定義する
    1. そのレコードが何の単位を表すか
  2. 他のテーブルとの関係を定義する
    1. 外部キーにより、他のテーブルとの参照関係を表す
    2. 外部キー+主キーにより、他のテーブルとの論理的な関係を表す

 下記のER図は、顧客が商品を注文するDBの事例です。これをもとに具体的に説明していきます。

主キー設計を行う具体的な事例として、顧客が商品を注文するDBのER図です。
ER図:主キー設計の例
ER図の見方を示した凡例です
ER図の凡例

レコードの意味を定義する(1-a)

 前回の記事「正規化のすすめ – 設計編(その2 – テーブル分割)」で、テーブル分割の観点を以下のように説明しました。

  • データの性質に基づきテーブルを分割する
    • データの性質にはマスタ・トランザクション・サマリの種類がある
    • 異なる性質のデータを同じテーブルに混在させてはならない
  • 意味的にまとまりでテーブルを分割する
    • 意味的に関連性のないデータは別テーブルに分割する
    • 適度にテーブルを分割することで、レコードサイズを抑える
    • アクセス頻度の低いカラムを別テーブルとすることで、I/Oの無駄をなくす
    • NOT NULL制約を適切に設定することで、無駄なNULLチェックの必要をなくす
  • ライフサイクルに基づきテーブルを分割する
    • ライフサイクルの異なるデータを同じテーブルに混在させてはならない

 これを主キー設計と組み合わせることで、データの意味や性質を詳しく定義することができます。

  1. テーブルおよび主キーの名前は、レコードの「単位」を定義する
  2. 主キーは、それが表すレコードについて以下のことを保証する
    • そのレコードが有効なデータとして存在する
    • そのレコードの属性が確実に設定されている(NOT NULL制約)
    • 同じ本質を表すデータは冗長に存在しない(UNIQUE制約)

 具体例として、顧客マスタを次のように定義しています。

顧客マスタの主キー設計の事例を表すER図です。
ER図: 顧客マスタの例
  • 顧客マスタのレコードが表す単位は、「1人の顧客」である
    • 「1人の顧客」を構成する必要最小限の情報は、顧客番号・ログイン情報(ログインID・パスワード)・表示情報(氏名)のセットである
      • 顧客の配送先は、顧客マスタとは意味・ライフサイクルが異なるため、別テーブルとする(詳細は2-aで説明)
      • 顧客の連絡先は、顧客マスタとは意味・ライフサイクルが異なるため、別テーブルとする(詳細は2-aで説明)
  • 主キー「顧客ID」は、そのレコードについて以下のことを保証する
    • 顧客マスタ上に存在する顧客は取引のある顧客である。取引停止した顧客は存在しない
    • 登録された顧客には、顧客番号・ユーザID・パスワードの情報が設定されている
    • 顧客マスタ上に、同じ顧客を表すレコードは複数存在しない
  • 顧客は、主キー(顧客ID)・顧客番号・ログインIDのいずれかで一意に特定できる
  • 顧客番号・ログインIDは主キーではないため、更新可能

他のテーブルとの関係を定義する(2-a)

 主キーに対し別のテーブルから外部キー制約を設定することで、テーブル間の関係を定義できます。
 これはテーブル間の論理的な関係を明示するとともに、データの論理的整合性を保つ必要がある場合に有用です。

  1. 外部キーを設定することで、そのテーブルが参照先テーブルと何らかの意味的な関係があることを表す
  2. 外部キーは、それが表すレコードについて以下のことを保証する
    • 外部キーに対応するレコードが、参照先テーブルに必ず存在する
    • 参照先のレコードを削除した場合、整合性を保つため以下のいずれかの状態となる(ON DELETE句)
      • 参照元のレコードも一緒に削除する(ON DELETE CASCADE)
      • 参照元の外部キーをNULLに更新する(ON DELETE SET NULL)
      • 参照先のレコードの削除を拒否する(ON DELETE NO ACTION)
  3. 外部キーと主キーの組み合わせで、参照先テーブルとの数関係を定義する
    • 外部キーを主キーに含めない場合は、参照先テーブルと1:0または1:nの関係となる
    • 外部キーを単一主キーとする場合は、参照先テーブルと1:0または1:1の関係となる
    • 外部キーを複合主キーに含める場合は、参照先テーブルと1:0または1:nの関係となる(詳細は2-bで説明)

 具体例として、顧客配送先マスタと顧客連絡先マスタを次のように定義しています。

顧客配送先マスタと顧客連絡先マスタの主キー設計の事例を表すER図です。
ER図:顧客配送先マスタと顧客連絡先マスタの例
  • 顧客配送先マスタのレコードが表す単位は、顧客に対する「1件の配送先」である
    • 「1件の配送先」を構成する必要最小限の情報は、配送先情報(郵便番号・住所・電話番号)のセットである
      • 顧客配送先は「顧客」そのものではなく、その付帯情報として「配送先」を表すので、顧客マスタとは別テーブルとする
      • 顧客配送先は、顧客マスタとライフサイクルが異なるケースを想定して別テーブルとする
        • 顧客登録時に配送先を登録するケース
        • 注文時に配送先を登録するケース
        • 顧客が設定画面から配送先を追加・変更するケース
  • 「顧客ID」は主キーではないため、1件の顧客に対し複数の配送先を登録できる(1:0または1:nの関係)
  • 外部キー「顧客ID」は、以下のことを保証する
    • 顧客マスタに対応する顧客のデータが存在する
    • 参照先の顧客のレコードが削除された場合、配送先のレコードも一緒に削除する(ON DELETE CASCADEを指定)
  • 顧客連絡先マスタのレコードが表す単位は、顧客に対する「連絡先」である
    • 「連絡先」を構成する必要最小限の情報は、連絡先情報(メールアドレス・電話番号)のセットである
      • 顧客連絡先は「顧客」そのものではなく、その付帯情報として「連絡先」を表すので、顧客マスタとは別テーブルとする
      • 顧客連絡先は、顧客マスタとライフサイクルが異なるケースを想定して別テーブルとする
        • 顧客登録時に連絡先を登録するケース
        • 注文時に連絡先を登録するケース
        • 顧客が設定画面から連絡先を設定するケース
  • 「顧客ID」が単一主キーとなっているため、1件の顧客に対し連絡先は1件のみ登録できる(1:0または1:1の関係)
  • 外部キー「顧客ID」は、以下のことを保証する
    • 顧客マスタに対応する顧客のデータが存在する
    • 参照先の顧客のレコードが削除された場合、連絡先のレコードも一緒に削除する(ON DELETE CASCADEを指定)

他のテーブルとの関係を定義する(2-b)

 2-aの「iii.外部キーと主キーの組み合わせで、参照先テーブルとの数関係を定義する」で説明を省略したパターンについて説明します。

 複合主キーと外部キーを組み合わせることで、より複雑なテーブル間の関係と論理的整合性を定義できます。

  1. 外部キーと主キーの組み合わせで、参照先テーブルとの数関係を定義する
    • 外部キーを主キーに含めない場合は、参照先テーブルと1:0または1:nの関係となる
    • 外部キーを単一主キーとする場合は、参照先テーブルと1:0または1:1の関係となる
    • 外部キーを複合主キーに含める場合は、参照先テーブルと1:0または1:nの関係となる
      • 外部キー+複合主キーで複数のテーブルを参照することで、n:mの関係を表現する

 具体例として注文明細トランザクションでは、次のように定義しています。

注文明細トランザクションの主キー設計の事例を表すER図です。
ER図:注文明細トランザクションの例
  • 注文明細トランザクションのレコードが表す単位は、注文に対する「1件の明細」である
    • 「1件の明細」を構成する情報は、対象の注文・対象商品の情報(商品ID・注文数・注文時単価)のセットである
    • 複合主キー「注文ID・商品ID」は、そのレコードについて以下のことを保証する
      • 1件の注文と1件の商品で1件の明細を表す
      • 1件の注文の中で、同じ商品の明細は複数登録できない(UNIQUE制約)
      • 明細について、注文と商品は必須情報である(NOT NULL制約)
    • 外部キー「注文ID」は、以下のことを保証する
      • 指定した注文IDは有効な注文である(注文トランザクションに存在する)
    • 外部キー「商品ID」は、以下のことを保証する
      • 指定した商品が取り扱い対象商品である(商品マスタに存在するので廃番の商品ではない)
    • 「注文ID」と「商品ID」は複合主キーであるため、注文と対象商品の関係は以下のようになる
      • 1件の注文の中で複数の商品を指定できる。また、1件の商品は複数の注文を受けることができる(n:mの関係)

 複合主キーを用いた定義は有用ですが、一方で使いすぎると以下の副作用があります。

  • 仕様変更でテーブル間の関係が変わる場合、主キーの変更に困難が伴う
  • 複合主キーを大量に組み合わせるとテーブル間の依存関係が複雑になり、かえって意味が不明確になる
  • 複雑な複合主キーは、それを扱うSQLが複雑になる

 そのため複合主キーの設計では、以下の点も考慮すると良いでしょう。

  • 将来的にも不変であることを保証できる関係にだけ、複合主キーを用いる
  • 複合主キーが複雑になり過ぎる場合は、適宜サロゲートキーを用いて依存関係を断ち切る

主キー設計がもたらす効果

 以上のパターンを組み合わせて主キーを設計することで、設計の意図を明確にするとともに、論理的整合性が自然に保たれるデータベースを構築できます。
 これは次のような効果をもたらします。

  • 仕様変更や機能追加に強いシステムを実現できる
  • プログラムの実装がシンプルになり、性能・品質が良好になる

 一方で主キー設計に考慮不足や誤りがあると、次のような問題が発生します。

  1. データの一貫性と整合性が保証されない
    • 例:同じ商品を表す情報が複数存在する
    • 例:予約がダブルブッキングする
  2. 意図しない仕様上の制限が発生する
    • 例:配属が決まるまで新入社員を登録できない
    • 例:講義マスタの複合主キーを構成する情報(学部・学科・学年など)を持たない聴講生は、
        リレーションを設定できないので講義を受講できない
  3. 適切なリレーションを定義できなくなる
    • 例:複雑な複合主キーのテーブルを参照するために、様々なテーブルからキーを
        集めてくる必要が発生する
    • 例:意味的に無関係なはずのテーブルとの外部キー制約が発生する
  4. SQLが不必要に複雑化する
    • 例:曖昧さを排除するために無効データ・不整合を除外するロジックが必要になる
    • 例:複合キーが増えすぎてJOIN条件が複雑になる

 こうした問題はシステムの拡張性と柔軟性を損なうとともに、性能・品質の低下を引き起こします。
 さらに主キーの設計変更には、長時間のシステム停止・大量のデータ更新・大規模なプログラム修正といった大変な困難が伴います。

 したがって主キー設計では、将来的な想定まで含めて考慮不足が起こらないように検討することが重要です。

ナチュラルキーかサロゲートキーか

 主キーを定義する際に、ナチュラルキーとサロゲートキーのどちらを使うかで意見が分かれます。

ナチュラルキー: 運用上意味のあるデータに基づくキー(従業員番号・商品コードなど)
サロゲートキー: 運用上の意味を持たず機械的に採番したキー

ナチュラルキーを主キーとする場合の例を示したER図です。
利用者管理番号・貸出番号・蔵書管理番号が主キーとなっており、外部キーもこれに基づいて設定されます。
ER図:ナチュラルキーを用いた設計例(図書館)
サロゲートキーを主キーとする場合の例を示したER図です。
利用者管理番号・貸出番号・蔵書管理番号はUNIQUE制約を持つ属性となっており、外部キーはサロゲートキーに基づいて設定されます。
ER図:サロゲートキーを用いた設計例(図書館)

 ナチュラルキーとサロゲートキーの議論は長年続いており、決着がつかないようです。
 しかし設計上、どちらを使うかを選択しなければなりません。

 一般的には、ナチュラルキー派が多いようです。両方を適宜使い分けるハイブリット派もいます。
 どちらを使うかは自由ですが、ここでは以下のようにサロゲートキーを使用することをお勧めします。

  1. サロゲートキーを主キーとする
  2. ナチュラルキーは通常のカラムとして定義し、UNIQUE制約を設定する
  3. テーブル間の関係を定義したい場合は、サロゲートキー同士の複合主キーとして定義する

 サロゲートキーを勧める理由を5つ挙げます。

  1. 不変性を保証できるナチュラルキーは存在しない
    • 主キーの要件は「一意かつ不変であること」なので、ナチュラルキーの不変性が崩れると、大規模なデータ修正やシステム改修が必要になる
    • 例えば「従業員番号」には不変性があるように思えるが、以下のような想定外の可能性は否定できないので、不変性を保証できない
      • 会社の合併などで番号体系を変えざるを得なくなる
      • オペミスや不具合で間違った値が登録され、修正が必要になる
      • 以下のような規格や国で定めたコード体系であっても永遠に不変である保証はなく、変更リスクは存在する
        • 商品のバーコード(JANコード)
        • 運転免許証番号
  2. ナチュラルキーは柔軟性と拡張性を損なう
    • ナチュラルキーの場合、複合キーが多発する
    • 複合キーを多用しすぎると、意図しない仕様上の制約が発生する
    • 業務要件の変更に伴い、複合キーの定義変更が発生する
  3. サロゲートキーには柔軟性と拡張性がある
    • サロゲートキーの場合、単一キー/複合キーのどちらにするか任意に選択できる
      • 適切に複合キーを使えば、テーブル間の論理的整合性を保証できる
      • 適切に複合キーと単一キーを選択すれば、業務要件の変更の影響を受けない
    • すべてのテーブルの主キーを単一のサロゲートキーで定義すること(いわゆる「ID required」)は過剰なので、適切に複合キーを使用する
  4. 主キーに可読性を求めるべきではない
    • サロゲートキーのデメリットとして「サロゲートキーは無機質なコードなので可読性に乏しい」と言われている
      • 内部処理のために最適化した正規形データに対して、人間のための可読性を求めることは適切ではない。必要に応じてSQLで可読性の良い形式に変換すべきである
  5. サロゲートキーによるレコードサイズの増加は許容範囲内である
    • サロゲートキーのデメリットとして、「サロゲートキーとナチュラルキーの両方を持つことでレコードサイズが増大する」と言われている
      • 正規化してレコードサイズを抑えるように心がけていれば、数十バイト程度の増加はそれほど性能に影響しない
      • ナチュラルキーにおける複合キーの多発の方が、レコードサイズへの影響は大きい

 上記の議論を詳しく解説し出すと非常に長くなるため、ここでは端的にまとめました。もし機会があれば別の記事で解説したいと思います。

 私のここ20年くらいの経験では、a.やb.の問題で行き詰まっているシステムをよく見かけました。
 一方で私はサロゲートキーを選択し続けていますが、特に行き詰まったことはありません。

 個別のノウハウや事情もあると思いますので、絶対にサロゲートキーであるべきとは言いません。特にナチュラルキーで構築したDBをサロゲートキーに切り替えるのは、非常に困難でもあります。
 ナチュラルキーを使いたい方は、a.やb.の点に留意しつつ、採用する選択肢もあるでしょう。

管理コードがもたらす副作用

 システムを設計する上で、顧客が業務上で定義したコードが登場することがよくあります。
 例えば、次のようなコードです。

  • 商品コード
  • 学生番号
  • 講義コード

 これらはナチュラルキーの一種でもあります。ここではこのようなコードを「管理コード」と呼ぶことにします。

 こういった管理コードは、何らかの構造を持っていることが多いでしょう。例えば、次のような構造です。

商品コード: [商品カテゴリコード(3桁)][製造部署コード(3桁)][通番(6桁)]
学生番号:  [入学年(4桁)][学部コード(2桁)][通番(4桁)]
講義コード: [学部コード(2桁)][年度(4桁)][授業コード(6桁)][枝番(2桁)]

 こうした管理コードの定義は、業務上の必要や歴史的背景から発生しています。したがって、管理コードの存在自体を問題にすべきではありません

 しかしながら管理コードを以下のように使い始めると、副作用が発生します。

  1. 管理コードを主キーにする
    • 主キーは更新できないので、管理コードの変更に対応できない
      • 例:商品の製造部署が変わると、商品コードに製造部署が含まれるため変更が必要になる
      • 例:学生の転学部が発生すると、学生番号に学部が含まれるため変更が必要になる
  2. プログラムが管理コードの構造を解釈する
    • 例:講義コードの上2桁目で対象学部を判断する
      • 例外として複数の学部をまたいで開講される講義は、講義コードの上2桁では対象学部を判断できない。講義の対象学部マスタを検索すれば、正しく判定できる。
    • 例:顧客が管理コードの定義を逸脱したコードを入力する
      • プログラムが未定義の値を解釈して、想定外の動作を引き起こす

 1.では逸脱できない仕様上の制限が発生していることが問題です。
 しかしこの制限は顧客にとっては理不尽な内容であり、仕様として受け入れられない可能性があります。
 どうしても対応が必要な場合は、過去データとの一貫性を保つためにデータの再登録が必要になりますが、この対応は一般的に大変な労力を伴います。

 2.では管理コード自体がひとつのテーブルとして機能しており、これは関数従属の状態です。
 この状態で管理コードとリレーションの間で不整合が発生したとき、リレーションに基づき判定する処理は正しく動作しますが、管理コードに基づき判定する処理が混入していると、不具合や想定外の動作を引き起こします。

 こうした副作用に対応するため、影響箇所の精査と対処に奔走する状況をよく見かけます。
 この状態は、せっかくの正規化が破壊されてしまった状態です。

サロゲートキーを用いた解決方法

 管理コードの副作用は、サロゲートキーを用いることで解決します。
 「ER図:サロゲートキーを用いた設計例(図書館)」では、以下のように定義しています。

  • 主キーをサロゲートキーで定義する
  • 管理コードは、主キーではなくUNIQUE制約を持つ属性として定義する
  • プログラムで管理コードの構造を解釈せず、その代わりにリレーションで表現する

 このように定義することで、以下の効果があります。

  • 管理コードは一意性だけ守れば更新可能になる
  • 管理コードの更新が起きても、1レコードの更新で全体の一貫性が保たれるため、プログラム修正や大量のテーブル更新を行う必要がなくなる
  • リレーションは管理コードよりも関係を精密に表現できるので、仕様に拡張性と柔軟性が生まれる

 管理コードの定義は顧客の企業文化でもあり、それ自体を否定することは適切ではありません。しかしその定義に不変性と正確性がなければ、設計として依存すべきではありません。

 ここで挙げた管理コードの扱いはナチュラルキーの議論にもつながりますので、おそらく賛否両論あるかと思います。
 一度決めた主キー設計を見直すことは難しいという事情もあると思いますが、少なくともそれが引き起こす副作用については、理解しておくべきです。

まとめ

 今回は、正規化以外に必要な設計のひとつとして、主キー設計について解説いたしました。

 まとめとして、主キー設計では以下の観点が重要になります。

  1. 主キー設計の目的を理解して適切に定義する
    • レコードの意味を定義する
    • テーブル間の関係(リレーション)を定義する
  2. ナチュラルキーとサロゲートキーの選択
  3. 管理コードの副作用に注意

 ここまでデータベースの正規化について掘り下げてきました。
 「正規化のすすめ – 設計編」はまだ続きますが、データベースの話題ばかりでは面白くないと思いますので、次回は少し趣向を変えてシステム設計全般の話題を扱いたいと思います。


1つ星2つ星3つ星4つ星5つ星 (まだ評価がありません)
タイトルとURLをコピーしました