正規化 — 同じ事実を2か所に書かない
正規化は理論ではなく「更新時に矛盾が起きない形」を作る作業。どこまで正規化し、どこで意図的に崩すか。
この記事の進み方
What — 段階的に分解していく
正規化は「1つの事実を1か所にまとめる」という作業を、段階を踏んで進めるものです。
各段階は「どういう種類の重複を排除するか」で分かれている。実務では第3正規形まで到達すれば、更新時の矛盾はほぼ防げる。
正規化されていないと何が起きるか
orders に product_name を直接持つ。結合が不要で速いが、事実が複数箇所に散る。
orders
+----+------------+---------------------+-------+
| id | product_id | product_name | price |
+----+------------+---------------------+-------+
| 1 | 10 | ワイヤレスイヤホン Pro | 15000 |
| 2 | 10 | ワイヤレスイヤホンPro | 15000 | ← 表記ゆれ
| 3 | 10 | ワイヤレスイヤホン Pro | 12000 | ← 値も違う
+----+------------+---------------------+-------+- 読み取り
- 結合不要で速い
- 更新時の矛盾
- 起きる
- 商品名の変更
- 全行を更新する必要
- 未注文の商品
- 登録できない
- –商品名を変えるには、その商品の全注文行を UPDATE する必要がある。1行でも漏れると分裂する。
- –まだ1件も注文がない商品は、このテーブルに存在できない(挿入時異常)。
- –最後の注文を削除すると、商品の情報も消える(削除時異常)。
正規化の効果は「速さ」ではなく「矛盾が起きえない構造」。速さについては、非正規化のほうが有利なことも多い。だから判断が必要になる。
- 関数従属
- ある列の値が決まれば、別の列の値も決まる関係。product_id が決まれば product_name も決まる。
- 部分関数従属
- 複合主キーの一部だけで決まってしまう関係。第2正規形で排除する。
- 推移的関数従属
- キー → 中間の列 → その列、と2段階で決まる関係。第3正規形で排除する。
- 更新時異常
- 同じ事実が複数行にあるため、一部だけ更新されて矛盾すること。
- 挿入時異常
- 関連するデータがないと登録できないこと。注文がない商品を登録できない、など。
- 削除時異常
- ある行を消すと、消すつもりのなかった情報まで失われること。
正規化が主に守っているのはどれですか?
Why — 正規化が守っているのは「更新」
正規化の目的は速さではありません。同じ事実を1か所にしか置かないことで、更新時の矛盾を構造的に不可能にすることです。
冒頭の事故を思い出してください。商品名を変更する処理を書いた人は、products テーブルを更新しました。それは正しい。orders にもコピーがあることを知らなかっただけです。
人間が「全部のコピーを更新する」ことに期待する設計は、必ず破れます。 コピーがそもそも存在しなければ、破れようがありません。
では、なぜ非正規化するのか
正規化が正しいなら、なぜ実務で非正規化するのでしょうか。理由は2つあります。
理由1:読み取りの性能
正規化を進めるほどテーブルが増え、結合が増えます。5テーブルの結合を毎秒1万回実行するような場面では、非正規化が現実的な選択になります。
ただしこれは最後の手段です。まずインデックス、次にキャッシュ、それでも足りなければ非正規化。順序を飛ばして非正規化から入ると、性能が出ないうえに矛盾も抱えることになります。
理由2:そもそも「同じ事実」ではない
こちらのほうが重要です。冒頭の事故には、実は別の見方があります。
注文時の商品名は、products の現在の商品名と同じ事実でしょうか。
領収書に印字された商品名は、その時点の商品名です。商品名が後から変わっても、過去の領収書の記載が変わってはいけません。同様に、注文時の価格は「今の価格」ではなく「注文したときの価格」です。
つまり orders.product_name は、products.name のコピーではなく、独立した事実なのです。
冒頭の事故の本当の問題は、「商品名をコピーしたこと」ではなく、**「スナップショットのつもりなのか、キャッシュのつもりなのかが曖昧だったこと」**です。
- スナップショットなら → 商品名が変わっても更新してはいけない。表記ゆれは正常
- キャッシュなら → 商品名が変わったら全行更新しなければならない。更新漏れが事故
どちらのつもりかが決まっていないと、どちらの運用も正しくできません。
注文テーブルに購入時の商品名を持たせるのは、非正規化の失敗ですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この設計をどう直しますか?
- 飲食店のモバイルオーダーシステム。注文テーブルが以下の1枚だけになっている
- orders(id, table_no, item1_name, item1_price, item2_name, item2_price, item3_name, item3_price, staff_name, staff_phone, total, ordered_at)
- 1回の注文で4品以上頼まれることがあり、その場合は注文を分けて登録している
- 商品の価格は月替わりで変わる。過去の注文の金額は、注文時の価格でなければならない
- スタッフの電話番号が変わったとき、過去の注文データの電話番号も更新すべきか議論になっている
- 「今月、唐揚げは何食出たか」という集計を毎日行っている
- · 繰り返しのカラムがあることで、どんなクエリが書けなくなっているか
- · 商品価格とスタッフ電話番号は、同じ性質のデータか
- · 正規化すべき列と、スナップショットとして残すべき列をどう見分けるか
「結合が多くて遅いので、テーブルに列を写したい」という提案に、何を確認すべきかを説明してください
- 相手は速度を理由にしている
- 写す対象は他テーブルの現在値
- 反対ではなく、条件を詰めたい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。