データベース●●●○○

正規化

Normalization ・ せいきか

同じ事実を一か所にだけ書くよう表を分ける設計技法。更新・挿入・削除の異常を防ぐ。

✦ 30秒で試す — 航海引っ越し1回で、住所が2つある客が生まれてしまう田中さんの注文を取り消して、その行を消すと?予想してみる

概要

正規化は、RDBMSのテーブル設計で「同じ事実を二か所以上に書かない」ように、表を段階的に分けていく設計技法です。たとえば注文テーブルの各行に顧客の住所を毎回書いていると、顧客が引っ越したときにその顧客のすべての行を直さなければならず、直し漏れた行が「もう住んでいない住所」を主張し続けます。正規化は、こうした不整合を人の注意力ではなく、表の構造で防ぎます。

どこまで分けたかは第1正規形、第2正規形……という段階で表し、実務では第3正規形まで満たすのが基本線です。テーブル設計はアプリケーションの土台であり、あとから直すコストが最も高い部分です。正規化は、その土台の良し悪しを「どの列がどの列を決めているか」という検証できる基準で判定する、DB 設計の中心の語彙です。

なぜ生まれたか

1970年に Edgar F. Codd がリレーショナルモデルを提唱すると、データを自由に表として設計できるようになりました。すると今度は「どんな表の分け方が良い設計なのか」という問いが生まれます。何も考えずに1枚の大きな表へ何でも詰め込むと、同じ情報が何度も書かれ、更新のたびに不整合の危険を抱えます。その危険は3つの形で現れました。挿入異常(注文がまだない顧客を登録できない)、更新異常(重複した箇所の直し漏れ)、削除異常(最後の注文を消すと顧客の情報まで消える)です。

Codd 自身が1970〜71年に、この問題へ理論的な答えを与えました。「どの列の値が決まれば、どの列の値が決まるか」という関数従属を分析し、異常が起きない表の条件を第1〜第3正規形として定義したのです。その後、第3正規形でも残る異常を消すために Raymond Boyce と Codd がボイス・コッド正規形(BCNF、1974年)を、Ronald Fagin が「1つの行に独立した複数の事実が掛け合わされている」問題を扱う第4正規形(1977年)と第5正規形(1979年)を定義し、理論が積み上がりました。設計の良し悪しが個人のセンスではなく、検証できる基準で判定できるようになったことが、正規化理論の功績です。

詳細

何が重複を生むのか — 同じ事実が行ごとに繰り返される

正規化前の典型は、注文を1行ずつ記録する表に、顧客の名前と住所、商品の名前と単価を毎回書き込む形です。「佐藤さんは横浜市に住んでいる」「ペンは120円」という事実は、注文の数だけ繰り返されます。注文は注文ID が決まれば決まりますが、住所は顧客が決まれば決まり、単価は商品が決まれば決まります。1枚の表に、決まり方の違う事実が同居していることが、重複の根本の原因です。

正規化前: 注文テーブル注文ID顧客住所商品単価101佐藤横浜市ペン120102佐藤横浜市ノート300103田中大阪市ペン120104佐藤横浜市消しゴム80同じ事実が何度も書かれている佐藤さんの住所 → 3回、ペンの単価 → 2回1行だけ住所を直すと矛盾する(更新異常)注文103を消すと田中さんごと消える(削除異常)注文のない顧客は登録できない(挿入異常)決まり方ごとに表を分ける顧客顧客ID氏名住所C1佐藤横浜市C2田中大阪市住所は1か所だけ注文注文ID顧客ID商品ID101C1P1102C1P2103C2P1104C1P3商品商品ID商品名単価P1ペン120P2ノート300P3消しゴム80単価も1か所だけ
正規化の前と後。上の1枚の表では佐藤さんの住所が3回、ペンの単価が2回書かれている。下では事実ごとに表を分け、注文は顧客と商品を ID で参照する(1注文1商品に簡略化)

分けたあとは、顧客の事実は顧客テーブルに、商品の事実は商品テーブルに一度だけ書かれ、注文は顧客と商品を ID(外部キー)で指すだけになります。3つの異常を自分の手で起こし、列を別の表へ切り出すと何が変わるかを確かめてください。

⚡ 体験: 3つの異常を起こしてから、表を分ける
注文表から読み取れる顧客(実体はない)顧客住所(表から読むと)佐藤横浜市田中大阪市注文テーブル(1枚だけ)注文ID顧客住所商品101佐藤横浜市ペン✕102佐藤横浜市ノート✕103田中大阪市ペン✕104佐藤横浜市消しゴム✕右の枠へ運ぶと別の表に切り出す「顧客・住所」の列表へ運んで登録する新しい顧客: 鈴木・神戸市
○ 更新異常(住所が2通り)○ 削除異常(顧客ごと消える)○ 挿入異常(注文のない顧客は入らない)○ 切り出した後は1か所で済む

佐藤さんが川崎市へ引っ越しました。注文表の住所のセルを1つ押して書き換えてください。

第1〜第3正規形

第1正規形(1NF) は、すべてのマス目に単一の値だけが入っている状態です。1つの列に「080-xxxx, 090-yyyy」のように複数の値を詰め込んだり、「商品1、商品2、商品3…」という繰り返しの列を持ったりしない、という表としての最低条件です。繰り返しは列ではなく行に展開します。

第2正規形(2NF) は、複合主キーの一部だけで決まる列を分けた状態です。注文明細(主キー: 注文ID+商品ID)に商品名を持たせると、商品名は商品ID だけで決まるので、同じ商品名が明細の数だけ重複します。商品テーブルへ分けることで解消します。

第3正規形(3NF) は、主キー以外の列を経由して間接的に決まる列(推移的関数従属)を分けた状態です。注文テーブルに顧客ID と住所を両方持たせると、住所は「注文ID → 顧客ID → 住所」と間接的に決まるので、顧客テーブルへ分けます。ここまで来ると「一つの事実は一つの場所に」がほぼ実現され、更新異常はほとんど起きなくなります。

すべての列を持つ1枚の大きな表繰り返しを行に展開(第1正規形)主キーの一部で決まる列を別表へ(第2正規形)主キー以外を経由して決まる列を別表へ(第3正規形)重複のない複数の表 + 外部キーの参照
正規化の進行。関数従属を手がかりに表を段階的に分けていく

第3正規形でも残る異常 — BCNF と第4正規形

「ほとんど」と書いたのは、第3正規形でも異常が残る場合があるからです。たとえば「受講」テーブル(学生・科目・教員)で、「各教員は1つの科目しか教えない」「学生は科目ごとに1人の教員に習う」という規則があるとします。主キーは(学生, 科目)で、教員が決まれば科目が決まります。この表は第3正規形を満たしますが、「田中先生は数学担当」という事実が受講者の数だけ繰り返され、田中先生の担当を変えると直し漏れが起こります。原因は、候補キーではない列(教員)が別の列を決めていることです。すべての「決める側」が候補キーであることを求めるのが BCNF で、教員と科目の表を分けると解消します。ただし BCNF へ分けると「学生は科目ごとに1人の教員」という規則が1つの表の制約として書けなくなることがあり、実務では第3正規形で止める判断もあります。

第4正規形は、1人の社員の「資格」と「担当顧客」のように、互いに関係のない複数の事実を1つの表に掛け合わせて書くと、組み合わせの数だけ行が増える問題を扱います。独立した事実はそれぞれ別の表に分けます。

分けた表は JOIN で組み立て直す

正規化で分けた表は、SQL の JOIN で必要なときに元の形へ組み立て直せます。「分けても情報は失われない(無損失分解)」ことが正規化理論の前提で、外部キーでつながった表を結合すれば、顧客名つきの注文一覧はいつでも作れます。つまり正規化は「保存するときは重複なく、読むときは結合して」という役割分担です。書き込みの多い業務処理(OLTP)で正規化が基本になるのは、1回の更新が1か所で済み、トランザクションも小さく保てるからです。

非正規化というトレードオフ

一方で、正規化を進めるほど読み取りの JOIN が増え、表の多い複雑なクエリでは性能が問題になることがあります。そこで、意図的に重複を持たせる非正規化という選択肢があります。集計結果を別の列に持つ、よく参照する名前を子テーブルにも持たせる、といった手法です。大量のデータを集計するデータウェアハウスでは、JOIN を減らすために非正規化したスタースキーマが標準的に使われます。

ただし順序が重要です。まず正規化した設計を作り、実測で遅いと分かった箇所について、インデックスやキャッシュ、マテリアライズドビューで解決できないかを検討し、最後の手段として非正規化します。最初から「速そうだから」と重複を許すと、正規化が防いでいたはずの更新異常を、アプリケーションのコードで防ぎ続けることになります。非正規化は「不整合の危険を引き受けて速さを買う」取引であり、重複させた値をいつ・誰が同期するのかを決めてから行います。

なお、NoSQLのドキュメント型データベースでは、JOIN を避けるために関連データを1つのドキュメントへ埋め込む(意図的に非正規化する)設計が標準的です。正規化の理屈を知っていると、「何を諦めて何を得ているのか」がはっきり見えるようになります。

試問

注文テーブル1枚に、注文ID・顧客・住所・商品が書かれています(佐藤さんは3件、田中さんは注文103の1件だけ)。住所のセルを押すと書き換わり、✕ で注文を消せます。「顧客・住所」の列の束を顧客の枠へ運ぶと、別のテーブルに切り出せます。佐藤さんが横浜市から川崎市へ引っ越し、田中さんは注文を取り消したいと連絡してきました。

✦ 命題挑戦中

佐藤さんの引っ越し(川崎市)を食い違いなく反映し、田中さんの唯一の注文を取り消しても、田中さんの住所を失わないようにせよ

  • 佐藤さんの住所が、どこから読んでも川崎市だけ
  • 田中さんの唯一の注文 103 を取り消す
  • それでも田中さんの住所(大阪市)が残っている
⚡ 体験: 3つの異常を起こしてから、表を分ける
注文表から読み取れる顧客(実体はない)顧客住所(表から読むと)佐藤横浜市田中大阪市注文テーブル(1枚だけ)注文ID顧客住所商品101佐藤横浜市ペン✕102佐藤横浜市ノート✕103田中大阪市ペン✕104佐藤横浜市消しゴム✕右の枠へ運ぶと別の表に切り出す「顧客・住所」の列表へ運んで登録する新しい顧客: 鈴木・神戸市

佐藤さんが川崎市へ引っ越しました。注文表の住所のセルを1つ押して書き換えてください。