MySQL | SQLite経験者向け、30日で習得するMySQL:パフォーマンスと設計 - Day20 正規化と非正規化

SQL MySQL
スポンサーリンク

Day20 前半のゴール

「“正規化が正義”でも“非正規化が悪”でもなく、目的で選べるようになる」

今日は「正規化と非正規化」です。
ここは、実務に出たときにほぼ確実にぶつかるテーマです。

前半のゴールはこうです。

正規化とは何かを、難しい用語抜きで説明できる
非正規化とは何かも、ちゃんと“メリット・デメリット込み”で理解する
「どっちが正しいか」ではなく「状況で使い分けるものだ」と腑に落ちる

ここまで行けば、後半で「じゃあ実務でどうバランスを取るか」の話がスッと入ってきます。


正規化とは何か

「“同じ情報を何度も書かないように、きれいに分けること”」

教科書的には「第1正規形、第2正規形…」と難しい話が出てきますが、
まずはもっとラフに捉えてOKです。

正規化とは、一言で言うとこうです。

同じ意味の情報を、テーブルのあちこちに重複して持たないように、きれいに分けること

もう少し噛み砕くと、

「この情報は、このテーブルに1か所だけ持つ」
「他のテーブルからは、IDで参照する」

という設計の考え方です。

例えば、「ユーザー」と「都道府県」を考えてみます。

ユーザーのテーブルに、都道府県名をそのまま文字列で持つ設計もできますが、
正規化された設計では、だいたいこう分けます。

都道府県マスタテーブル(id, name)
ユーザーテーブル(id, name, prefecture_id)

「東京都」という文字列は、都道府県マスタに1回だけ出てくる。
ユーザー側は、そのID(たとえば 13)だけを持つ。

これが「同じ情報を1か所に集約する」という正規化の基本的な考え方です。


正規化のメリット

「更新が楽になる・矛盾が起きにくくなる」

正規化の一番のメリットは、「更新のしやすさ」と「データの一貫性」です。

さっきの都道府県の例で考えます。

もし、ユーザーテーブルに都道府県名を直接文字列で持っていたら、

「東京都」を「東京都(Tokyo)」に表記変更したい

となったとき、全ユーザーの行を更新しなければいけません。

100人ならまだいいですが、100万ユーザーいたら地獄です。
しかも、どこかに「東京」とか「東京都 」みたいな微妙な表記揺れが混ざっているかもしれません。

一方、正規化されていれば、

都道府県マスタの「東京都」の1行を更新するだけ

で済みます。

ユーザー側は prefecture_id = 13 のままなので、
アプリ側で JOIN して表示すれば、自動的に新しい表記になります。

ここでのポイントは、

正規化は「更新のコストを下げて、矛盾を防ぐための設計」

だということです。


正規化のデメリット

「読み取り時にJOINが増えて、クエリが重く・複雑になりやすい」

正規化は良いことばかりではありません。
実務で必ず出てくるデメリットがこれです。

読み取るときに、JOIN が増える

例えば、「ユーザー一覧を都道府県名付きで出したい」とします。

非正規化(ユーザーに都道府県名を直接持っている)なら、こうです。

SELECT id, name, prefecture_name
FROM users;
Python

正規化(都道府県マスタを分けている)なら、こうなります。

SELECT
  u.id,
  u.name,
  p.name AS prefecture_name
FROM users u
JOIN prefectures p ON u.prefecture_id = p.id;
Python

JOIN が1つ増えました。

JOIN が1つ増えるくらいなら大したことないように見えますが、
実務ではこれがどんどん積み重なります。

ユーザー
+都道府県
+会社情報
+部署情報
+権限マスタ

みたいに、正規化を突き詰めると、
「とにかくJOINだらけのクエリ」になりがちです。

JOIN が増えると、

SQL が読みにくくなる
インデックス設計が難しくなる
パフォーマンスチューニングの難易度が上がる

という副作用が出てきます。

ここで初めて、「非正規化」という選択肢が意味を持ち始めます。


非正規化とは何か

「あえて“重複”を許して、読み取りを速く・シンプルにする」

非正規化は、正規化の逆方向の考え方です。

あえて同じ情報を複数の場所に持つことで、
読み取りを速くしたり、クエリをシンプルにしたりする設計

例えば、注文テーブルを考えます。

正規化を突き詰めると、

users(ユーザー)
orders(注文ヘッダ)
order_items(注文明細)
products(商品マスタ)

のように分かれます。

注文履歴画面で「ユーザー名」「商品名」「商品単価」「注文時の合計金額」を出したいとき、
JOIN がかなり増えます。

そこで、実務ではよくこうします。

orders テーブルに、「注文時のユーザー名」「注文時の合計金額」を持たせる
order_items テーブルに、「注文時の商品名」「注文時の単価」を持たせる

つまり、

ユーザー名は users にもあるし、orders にもコピーされる
商品名は products にもあるし、order_items にもコピーされる

という状態になります。

これは、正規化の観点から見ると「重複」です。
でも、実務ではよくやります。

なぜかというと、

履歴画面を出すときに、JOIN を減らせる
過去の注文時点の情報(当時の価格・当時の商品名)をそのまま残せる

というメリットがあるからです。


非正規化のメリット

「読み取りが速くなる・履歴が“当時のまま”残せる」

非正規化のメリットは、大きく2つあります。

1つ目は、読み取りの速さです。

さっきの注文履歴の例で言えば、

orders にユーザー名と合計金額が入っていれば、
ユーザー名付きの注文一覧は orders だけで出せます。

SELECT
  id,
  user_name,
  total,
  created_at
FROM orders
WHERE user_id = 123
ORDER BY created_at DESC;
Python

JOIN がなくなるので、クエリがシンプルになり、
インデックス設計もしやすくなります。

2つ目は、「履歴の正しさ」です。

もし、商品名や価格を products だけに持っていて、
order_items には product_id だけを持つ設計だと、

商品名を変更したとき
価格を変更したとき

過去の注文履歴の表示も変わってしまいます。

「当時は 980円だったのに、今は 1200円になっている」
「当時は『お試しセット』という名前だったのに、今は『スターターパック』になっている」

こういうケースで、

注文時点の価格・商品名を order_items にコピーしておけば、
履歴は「当時のまま」残せます。

これは、会計・請求・法務的にも重要になることが多いです。


非正規化のデメリット

「更新が難しくなり、矛盾リスクが上がる」

もちろん、非正規化にもデメリットがあります。

一番分かりやすいのは、「更新の難しさ」です。

例えば、ユーザー名を users と orders の両方に持っている場合、

ユーザー名を変更したら、
users も orders も両方更新しないといけない

もし、orders 側の更新を忘れたら、

users.name と orders.user_name で値が食い違う

という「矛盾」が発生します。

これを防ぐには、

アプリケーション側で「ユーザー名変更時に、関連するテーブルも全部更新する」ロジックを書く
トリガーで自動更新する(ただし複雑になりやすい)

などの工夫が必要です。

つまり、

非正規化は「読み取りを楽にする代わりに、更新を難しくする」

というトレードオフを持っています。


正規化 vs 非正規化は「勝ち負け」ではない

「“読み取り中心か”“更新中心か”“履歴をどう扱うか”で決める」

ここまで来ると、だんだん見えてくると思います。

正規化
→ 更新が楽・矛盾が起きにくい・でもJOINが増えがち

非正規化
→ 読み取りが速い・履歴を当時のまま残しやすい・でも更新が難しい

どちらが「正しい」か、という話ではありません。
大事なのは、システムの性質です。

読み取りが圧倒的に多いのか
更新が頻繁に行われるのか
履歴を「当時のまま」残す必要があるのか

例えば、

分析用のデータマート(BIツールで集計する用のDB)なら、
非正規化をガッツリやって、読み取り最優先にすることが多いです。

一方、マスタデータ管理(顧客マスタ、商品マスタなど)では、
正規化をしっかりやって、矛盾を極力減らすことが多いです。

Day20 前半で一番伝えたいのは、

正規化と非正規化は「どちらかを信仰するもの」ではなく、
「目的に応じて混ぜて使う道具」だということ

です。


Day20 前半のまとめ

正規化は「同じ意味の情報をテーブルのあちこちに重複させず、1か所に集約してIDで参照する」設計であり、都道府県マスタとユーザーテーブルのように分けることで「表記変更などの更新が1か所で済む」「データの矛盾が起きにくい」という大きなメリットがある一方、読み取り時にはJOINが増えてSQLが複雑になり、パフォーマンスチューニングも難しくなりやすい。
非正規化はその逆で、「あえて同じ情報を複数のテーブルにコピーして持つ」設計であり、注文テーブルにユーザー名や合計金額、注文明細に商品名や単価を持たせることで「JOINなしで履歴画面を出せる」「商品名や価格が変わっても、注文時点の情報をそのまま残せる」といった実務的なメリットがあるが、その代わりに「更新時に複数テーブルを揃えて直さないと矛盾が起きる」というリスクも背負うことになる。
結局のところ、正規化と非正規化は「どちらが正しいか」ではなく、「更新のしやすさ・矛盾の少なさを優先するのか」「読み取りの速さ・履歴の扱いやすさを優先するのか」といったシステムの性質や要件に応じてバランスを取るための選択肢であり、Day20 後半では「どこまで正規化して、どこから非正規化するか」を具体的な判断軸として整理していきます。

タイトルとURLをコピーしました