正規化
データの重複をなくすようテーブルを分割する設計技法。更新時の不整合を防ぐ。
概要
正規化は、RDBMSのテーブル設計において「同じ事実を二か所以上に書かない」よう、データの重複が生まれないテーブル構造へ段階的に分割していく設計技法です。たとえば注文テーブルの各行に顧客の住所を毎回書いていると、顧客が引っ越したときにすべての行を直さなければならず、直し漏れた行が「もう存在しない住所」を主張し続けます。正規化は、こうした更新時の不整合(更新異常)を構造の力で防ぎます。
正規化の到達度は第1正規形、第2正規形……と段階で表され、実務では第3正規形までを満たすのが基本線です。テーブル設計はアプリケーションの土台であり、あとから直すコストが最も高い部分です。正規化はその土台の品質を判定する、DB設計の中心的な語彙です。
なぜ生まれたか
リレーショナルモデルの登場でデータを自由に表として設計できるようになると、今度は「どんな表の分け方が良い設計なのか」という問いが生まれました。何も考えずに1枚の大きな表へ何でも詰め込むと、同じ情報の重複だらけになり、更新のたびに不整合の危険を抱えます。挿入異常(注文がまだない顧客を登録できない)、更新異常(重複箇所の直し漏れ)、削除異常(最後の注文を消すと顧客情報まで消える)という3つの異常が、場当たり的な設計に必ずついて回りました。
リレーショナルモデルの提唱者コッド自身が、この問題に理論的な答えを与えました。「どの列がどの列を決定するか」という関数従属の分析に基づき、異常が起きない構造の条件を正規形として定義したのです。設計の良し悪しが個人のセンスではなく、検証可能な基準で判定できるようになったことが正規化理論の功績です。
詳細
第1〜第3正規形
第1正規形(1NF) は、すべてのマス目に単一の値だけが入っている状態です。1つの列に「080-xxxx, 090-yyyy」のように複数の値を詰め込んだり、繰り返し項目(商品1、商品2、商品3…という列)を持ったりしない、という表としての最低条件です。
第2正規形(2NF) は、複合主キーの一部だけで決まる列を分離した状態です。注文明細(主キー: 注文ID+商品ID)に商品名を持たせると、商品名は商品IDだけで決まるため、同じ商品名が明細の数だけ重複します。商品テーブルへ分離することで解消します。
第3正規形(3NF) は、主キー以外の列を経由して間接的に決まる列(推移的関数従属)を分離した状態です。社員テーブルに部署IDと部署名を両方持たせると、部署名は「社員ID→部署ID→部署名」と間接的に決まるため、部署テーブルへ分離します。ここまで到達すると「一つの事実は一つの場所に(One Fact in One Place)」がほぼ実現され、更新異常は原則として起きなくなります。
正規化前後の構造を比べると、変化がよく分かります。正規化前は1枚の表に顧客・商品の情報が注文のたびに重複して書き込まれますが、第3正規形まで進めると「顧客の事実は顧客テーブルに、商品の事実は商品テーブルに」一度だけ書かれ、表同士は外部キーで参照し合う構造になります。
理論上はさらにボイス・コッド正規形、第4・第5正規形と続きますが、第3正規形まで適切に設計されていれば、それ以上の違反が問題になるケースは実務ではまれです。
正規化が防いでいる更新異常がどんなものか、実際に「直し漏れ」を起こして体感してみてください。
| 注文 | 顧客名 | 住所 | 商品 |
|---|---|---|---|
| 1 | 田中 | 東京都港区1-2-3 | キーボード |
| 2 | 佐藤 | 千葉県市川市9-8 | マウス |
| 3 | 田中 | 東京都港区1-2-3 | モニター |
| 4 | 田中 | 東京都港区1-2-3 | USBケーブル |
| 5 | 佐藤 | 千葉県市川市9-8 | ヘッドセット |
| 顧客テーブル | |
|---|---|
| 田中 | 東京都港区1-2-3 |
| 佐藤 | 千葉県市川市9-8 |
| 注文 | 顧客 | 住所(JOINで参照) | 商品 |
|---|---|---|---|
| 1 | 田中 | 東京都港区1-2-3 | キーボード |
| 2 | 佐藤 | 千葉県市川市9-8 | マウス |
| 3 | 田中 | 東京都港区1-2-3 | モニター |
| 4 | 田中 | 東京都港区1-2-3 | USBケーブル |
| 5 | 佐藤 | 千葉県市川市9-8 | ヘッドセット |
田中さんが引っ越しました。それぞれの「住所を変更」ボタンで UPDATE を1回だけ実行し、結果を見比べてください。
分割した表はJOINで再結合する
正規化で分割された表は、SQLのJOINによって必要なときに元の形へ組み立て直せます。「分割しても情報は失われない(無損失分解)」ことが正規化理論の前提であり、外部キーでつながった表を結合すれば、顧客名つきの注文一覧はいつでも作れます。つまり正規化は「保存するときは重複なく、読むときは結合して」という役割分担です。
非正規化というトレードオフ
一方で、正規化を進めるほど読み取り時のJOINは増え、表の数が多い複雑なクエリでは性能が問題になることがあります。そこで、意図的に正規形を崩して重複を持たせる非正規化という選択肢があります。集計結果を別列に持つ、参照頻度の高い名前を子テーブルにも持たせる、といった手法です。
ただし順序が重要です。まず正規化された設計を作り、実測で遅いと分かった箇所にだけ、インデックスやキャッシュで解決できないかを検討したうえで、最後の手段として非正規化する——これが定石です。最初から「速そうだから」と重複を許すと、正規化が防いでいたはずの更新異常をアプリケーションのコードで防ぎ続ける羽目になります。非正規化は「不整合のリスクを引き受けて速度を買う」取引だと理解しておくことが大切です。
なお、スキーマの柔軟さを掲げるNoSQLのドキュメント型データベースでは、JOINを避けるために関連データを1つのドキュメントに埋め込む(意図的に非正規化する)設計が標準的です。正規化の理屈を知っていると、「何を諦めて何を得ているのか」がクリアに見えるようになります。