AI開発

データベース正規化|5ステップで第3正規形へ

株式会社Atsumell|9分で読めます
データベース正規化を第3正規形まで5ステップで進める記事のサムネイル

はじめに

受注一覧を表計算で管理していた業務をシステム化するとき、その列をそのまま1つのテーブルへ移したくなる。しかし、顧客名を変更したのに過去の行だけ古い、商品単価を直したら当時の売上まで変わる、注文を削除したら顧客の連絡先も消える、といった問題が起きやすい。

こうした重複と更新時の矛盾を減らす考え方が、データベース正規化である。正規化はテーブルを細かく分ける作業ではない。「その事実は何についての事実か」を見極め、1つの事実を原則1か所で管理する設計だ。

この記事では、受注データを題材に、第3正規形までを5ステップで整理する。目的は用語の暗記ではなく、要件定義で業務ルールを確認し、ER図、テーブル定義、受け入れ条件へ渡せる状態にすることである。

データベース正規化とは

正規化は、属性間の依存関係に沿ってデータを分け、重複や不整合を抑える方法である。Microsoftのデータベース設計の基本では、第1正規形を繰り返しグループの排除、第2正規形を複合キーの全体への依存、第3正規形を非キー属性間の依存の排除として説明している。IBMの正規化ルールの要約も、前の正規形を満たしながら段階的に依存を整理する考え方を示している。

正規化で防ぎたい代表的な問題は3つある。

  • 更新時異常:同じ顧客名が複数行にあり、一部だけ更新される
  • 追加時異常:注文がない顧客を登録できない
  • 削除時異常:最後の注文を消すと顧客情報まで失われる

たとえば、次のような受注明細があるとする。

注文ID注文日顧客ID・顧客名商品1商品2
O-1019/30C-01・青空商事P-10・キーボード・2個P-20・マウス・1個

一見分かりやすいが、商品数が増えるたびに列を追加する必要があり、顧客名や商品名も注文ごとに重複する。ここから順に整理する。

第3正規形まで進める5ステップ

ステップ1:1行が何を表すか決める

最初にテーブル名や型を決めるのではなく、1行の意味を文章にする。上の表で「1行は1注文」と決めた場合、複数の商品が1行に押し込まれている。一方、「1行は1注文明細」とすれば、注文と商品の組み合わせが1行になる。

候補となる事実を分けると、少なくとも次の4種類がある。

  • 顧客という事実:顧客ID、顧客名、連絡先
  • 商品という事実:商品ID、商品名、現在の標準単価
  • 注文という事実:注文ID、注文日、顧客ID
  • 注文明細という事実:注文ID、明細番号、商品ID、数量、受注単価

ここで重要なのは、名前が似ていても意味を混ぜないことだ。「現在の標準単価」は商品についての事実だが、「受注時に合意した単価」は注文明細についての事実である。前者を更新しても過去の売上金額は変わらない設計にする。

この整理はデータディクショナリの作り方と一緒に進めるとよい。項目名だけでなく、意味、形式、必須条件、値の出所を記録すると、同名異義や別名同義を見つけやすい。

ステップ2:繰り返しをなくして第1正規形にする

第1正規形では、1つのセルに1つの値を置き、「商品1、商品2」のような繰り返し列をなくす。受注明細を行に分けると次のようになる。

注文ID明細番号注文日顧客ID顧客名商品ID商品名数量受注単価
O-10119/30C-01青空商事P-10キーボード28,000
O-10129/30C-01青空商事P-20マウス13,000

これで商品数に応じて行を増やせる。CSVやAPIでも配列として扱いやすくなる。ただし、注文日と顧客名が明細ごとに重複しており、まだ完成ではない。

実務では「住所を1セルに置くのは第1正規形違反か」のような議論に時間を使いすぎない。分割単位は利用目的で決める。都道府県別に検索・集計するなら分ける価値があるが、表示だけなら住所文字列として扱う選択もある。原子性は業務上の操作単位とセットで判断する。

ステップ3:部分関数従属をなくして第2正規形にする

注文明細の主キーを「注文ID+明細番号」とする。この複合キー全体で1件の明細を特定できる。しかし、注文日と顧客IDは注文IDだけで決まり、複合キーの一部にしか依存していない。これが部分関数従属である。

そこで、注文と注文明細を分ける。

注文

注文ID注文日顧客ID
O-1019/30C-01

注文明細

注文ID明細番号商品ID数量受注単価
O-1011P-1028,000
O-1012P-2013,000

注文日を修正するときは注文を1行だけ更新すればよい。明細を追加しても、注文日や顧客IDを複製しない。

複合キーを採用せず明細IDを単独の主キーにする設計でも、業務上の一意性は消えない。「同じ注文の同じ明細番号は重複不可」という制約を別途持たせる。代理キーを付けただけで依存関係が解決したと考えないことが大切だ。

ステップ4:推移的関数従属をなくして第3正規形にする

注文テーブルに顧客IDと顧客名を両方置くと、注文IDから顧客IDが決まり、顧客IDから顧客名が決まる。顧客名は注文IDへ間接的に依存している。これが推移的関数従属である。

顧客と商品を独立させると、次の4テーブルになる。

  • 顧客:顧客ID、顧客名、連絡先
  • 商品:商品ID、商品名、標準単価
  • 注文:注文ID、注文日、顧客ID
  • 注文明細:注文ID、明細番号、商品ID、数量、受注単価

顧客名は顧客テーブルの1か所で更新する。商品名も同様である。注文は外部キーで顧客を参照し、注文明細は注文と商品を参照する。

ここまで来たら、関係をER図の書き方で可視化する。顧客と注文は1対多、注文と注文明細も1対多、商品と注文明細も1対多になる。「顧客を削除できるか」「商品を廃止した後も過去明細を表示できるか」といったライフサイクルも確認する。

ステップ5:業務ルールと性能を重ねて設計を確定する

第3正規形になれば自動的に良いシステムになるわけではない。最後に、制約、履歴、性能、運用を重ねる。

確認点決めること
識別子採番方式、変更可否、外部公開の有無
一意性顧客コード、注文ID、注文内の明細番号
参照整合性顧客・商品を削除、無効化したときの扱い
履歴受注時の単価、名称、送付先をどこまで固定するか
NULL未入力、該当なし、不明をどう区別するか
性能検索条件、件数、更新頻度、必要な索引

特に履歴は、単純な重複排除と衝突しやすい。請求書に表示した顧客名や送付先を当時の状態で保存する必要があるなら、スナップショットとして注文側に保持する合理性がある。その場合は「正規化できていない」のではなく、履歴要件に基づく意図的な重複である。根拠と更新ルールを仕様に残す。

読み取り性能のために集計値や表示用データを重複保持する非正規化も選択肢になる。ただし、先に計測する。どのクエリが遅いか、許容応答時間はいくつか、更新時にどう同期するかを決めずにテーブルをまとめると、不整合だけが戻ってくる。

データがどの処理から作られ、どこで更新されるかはデータフロー図の書き方で確認できる。ER図が静的な関係を示すのに対し、DFDは読み書きの流れを示す。両方を照合すると、保存先のないデータや更新責任のない項目を発見しやすい。

正規化レビューのチェックリスト

設計レビューでは、正規形の名前を答えられるかより、次を確認したい。

  • 1行が表す業務上の事実を説明できる
  • 繰り返し列や区切り文字で詰め込んだ複数値がない
  • 主キーまたは業務上の一意条件が決まっている
  • 非キー項目がキー全体に依存している
  • 顧客名など別の非キー項目で決まる値が分離されている
  • 現在値と取引時点の履歴値を区別している
  • 外部キーの削除・無効化ルールが決まっている
  • 意図的な非正規化には、性能根拠と同期方法がある

AIにDDLやER図を生成させる場合も、この情報が入力になる。項目一覧だけを渡すと、AIはもっともらしい主キーや関係を補完するが、それが業務上正しいとは限らない。「1行の意味」「一意条件」「履歴」「削除時の扱い」を明示し、生成結果は具体的な登録・変更・削除シナリオで検証する。

正規化は業務ルールをデータへ翻訳する工程

データベース正規化の本質は、表を分割することではない。顧客、商品、注文、明細という事実を分け、それぞれを何が一意に決めるかを明らかにすることである。

第1正規形で繰り返しをなくし、第2正規形で複合キーの一部だけに依存する項目を分け、第3正規形で非キー項目間の依存を分ける。そのうえで履歴、制約、性能を重ねれば、更新に強く、理由を説明できるデータ仕様になる。

株式会社Atsumellでは、業務資料やヒアリング内容から、データの意味、関係、例外、受け入れ条件を整理し、AIと開発チームが読める仕様づくりを支援している。ER図や項目一覧はあるものの実装判断が残っている場合は、お問い合わせから相談してほしい。

正規化の前提となる業務ルールや例外を整理する段階では、Kakusillの活用も選択肢になる。

#データベース正規化#データ設計#要件定義#ER図#第3正規形