SQLite | ゼロからはじめるSQL、30日で習得するSQLite:データ操作・設計 - Day20 複数テーブル設計

SQL SQLite
スポンサーリンク

Day20 後半

「1枚テーブル」から「ちゃんと分かれたテーブル」へ分解してみる

前半では、第1〜第3正規形を
それぞれバラバラの例で見ました。

後半では、あえて「よくあるダメな1枚テーブル」からスタートして、
それを第1→第2→第3正規形っぽい形に分解していく流れを、
通しでイメージしてみます。

そのうえで、

どこまで正規化するか
あえて崩す「非正規化」との付き合い方
セキュリティ的に「正規化しておくと得するポイント」

まで触れていきます。


スタート地点:よくある「全部入り orders テーブル」

まず、わざと悪い例を用意します。

order_id | order_date  | user_name | user_email        | item1_name | item1_price | item2_name | item2_price
---------+-------------+-----------+-------------------+------------+-------------+------------+------------
1        | 2025-05-01  | 山田太郎  | taro@example.com  | りんご     | 100         | バナナ     | 150
2        | 2025-05-02  | 山田太郎  | taro@example.com  | みかん     | 200         | NULL       | NULL

問題を整理すると、こうなります。

同じユーザー情報が注文ごとに重複している
商品が2つまでしか持てない構造になっている
商品名・価格が横に増えていく(item3, item4…地獄)

このまま機能を足していくと、

メールアドレス変更のたびに全行 UPDATE
3つ目の商品を扱うたびにスキーマ変更
「どこが正しい商品情報なのか」分からなくなる

という未来が見えます。

ここから、正規化のステップを踏んでいきます。


第1正規形への分解

「繰り返し列を“行”に落とす」

まずは第1正規形、
「1セルに複数の値を詰め込まない」「繰り返し列をやめる」です。

商品部分を「1行1商品」に分解して、
注文と商品を分けて考えます。

注文そのものはこうします。

orders
order_id | order_date  | user_name | user_email
---------+-------------+-----------+-------------------
1        | 2025-05-01  | 山田太郎  | taro@example.com
2        | 2025-05-02  | 山田太郎  | taro@example.com

商品行はこうします。

order_items
order_id | item_name | item_price
---------+-----------+-----------
1        | りんご    | 100
1        | バナナ    | 150
2        | みかん    | 200

これで、

「注文1の全商品」→ order_items で WHERE order_id = 1
「バナナだけ値上げ」→ item_name で UPDATE

といった操作が、SQL で素直に書けるようになります。

まだ重複は残っていますが、
「繰り返し列を行に落とす」という第1正規形のゴールは達成です。


第2正規形への分解

「商品情報を products に切り出して、“本体”を1か所にする」

次に第2正規形、
「主キーの一部にだけ依存する列を分ける」です。

order_items を少し拡張して、item_id を導入します。

order_items
order_id | item_id | item_name | item_price
---------+---------+-----------+-----------
1        | 1       | りんご    | 100
1        | 2       | バナナ    | 150
2        | 3       | みかん    | 200

ここで主キーを (order_id, item_id) とすると、

quantity(もしあれば)は (order_id, item_id) に依存
item_name, item_price は item_id にだけ依存

という状態になります。

第2正規形では、
item_name, item_price を「商品マスタ」に切り出します。

products
item_id | item_name | item_price
--------+-----------+-----------
1       | りんご    | 100
2       | バナナ    | 150
3       | みかん    | 200

order_items はこうなります。

order_items
order_id | item_id
---------+--------
1        | 1
1        | 2
2        | 3

これで、

バナナの価格変更 → products だけ UPDATE
注文ごとの商品一覧 → order_items と products を JOIN

という、「商品情報の本体は products に1か所」
という状態が作れました。


第3正規形への分解

「ユーザー情報を users に切り出し、住所などもさらに整理する」

次に第3正規形、
「キー以外の列同士の依存を分ける」です。

orders を少しリッチにしてみます。

orders
order_id | order_date  | user_id | user_name | user_email
---------+-------------+---------+-----------+-------------------
1        | 2025-05-01  | 1       | 山田太郎  | taro@example.com
2        | 2025-05-02  | 1       | 山田太郎  | taro@example.com

ここで、

user_name, user_email は user_id に依存している
→ orders に重複して持つ必要はない

と分かります。

そこで、ユーザー情報を users に切り出します。

users
user_id | user_name | user_email
--------+-----------+-------------------
1       | 山田太郎  | taro@example.com

orders はこうシンプルになります。

orders
order_id | order_date  | user_id
---------+-------------+--------
1        | 2025-05-01  | 1
2        | 2025-05-02  | 1

さらに、住所情報まで持つとします。

users
user_id | zip_code | prefecture | city
--------+----------+------------+---------
1       | 100-0001 | 東京都     | 千代田区

ここで、

prefecture, city は zip_code に依存している
→ 第3正規形的には zip_codes に切り出せる

という話が前半でした。

zip_codes
zip_code | prefecture | city
---------+------------+---------
100-0001 | 東京都     | 千代田区

users は zip_code だけを持つ形にできます。

こうして、

ユーザーの本体 → users
住所の本体 → zip_codes
商品情報の本体 → products
注文の本体 → orders
注文ごとの商品行 → order_items

という、「それぞれの“本体”が1か所にある」構造 ができあがります。


正規化しすぎると何が起きるか

「JOIN だらけで、読むのも書くのもつらくなる」

ここまで聞くと、

「じゃあ、とにかく細かく分ければいいんだな」

と思いがちですが、
現実はもう少しバランスが必要です。

正規化を突き詰めすぎると、

SELECT のたびに JOIN が3段、4段と増える
シンプルな画面表示でも複数テーブルをまたぐ
開発者がテーブル構造を追いきれなくなる

といった「運用のつらさ」が出てきます。

例えば、

ユーザー一覧画面で、
名前・メール・都道府県・市区を出したいだけなのに、

users
JOIN zip_codes

が必須になる、などです。

この「JOIN が増えすぎてつらい」問題を避けるために、
あえて少しだけ正規化を崩すことがあります。

これが 非正規化(denormalization) です。


あえて崩す「非正規化」との付き合い方

「正規化を理解したうえで、意図的にルールを緩める」

非正規化は、

正規化のルールを知らないまま雑に崩すこと
ではなく、

正規化のルールを理解したうえで、
「ここはあえて重複を許した方がトータルで得」と判断して崩すこと

です。

例えば、

users に prefecture_name だけを持たせてしまう
(zip_codes から JOIN してもいいが、よく使うのでコピーしておく)

orders に user_name を持たせておく
(履歴として「当時の名前」を残したい、JOIN なしで見たい)

などが現実的な例です。

このとき大事なのは、

どの列が「本体」で、どの列が「コピー」なのかを意識する
コピー側は「多少古くてもいい」「矛盾しても許容」と割り切る

という線引きです。

「全部を常に完全一致させる」ことを目指すと、
非正規化はほぼ不可能になります。


セキュリティの視点から見る「正規化と非正規化の線引き」

セキュリティ的に見ると、

「真実の値がどこにあるか」が明確であること
(=正規化された“本体テーブル”があること)

はとても重要です。

監査や不正調査をするとき、
「このテーブルのこの列が正」と決められるかどうかで、
調査の難易度が大きく変わります。

一方で、非正規化されたコピー列は、

画面表示用のキャッシュ
履歴としての“当時の値”

など、用途を限定しておくと扱いやすいです。

セキュリティの観点からは、

本人確認や権限判定に使う値
ログイン・認証に関わる値

などは、できるだけ正規化された“本体”に集約し、
コピーを増やさない方が安全です。

逆に、

集計用のカウンタ
画面表示用の冗長な文字列

などは、非正規化してもリスクが低いことが多いです。


小さな練習イメージ

頭の中で、次のような1枚テーブルを想像してみてください。

ユーザー名・メール・住所・商品名・商品価格・数量・注文日が、全部1テーブルに入っている。

そこから、

ユーザーの本体はどこに切り出すか
商品情報の本体はどこに切り出すか
住所情報は郵便番号とどう分けるか

を順番に考えてみると、
第1〜第3正規形のステップが、だいぶ立体的に見えてくるはずです。


Day20 後半のまとめ

「全部入り1枚テーブル」から、第1→第2→第3正規形へ分解していくと、ユーザー・商品・住所・注文・注文商品が、それぞれ“本体テーブル”として整理される。
正規化のゴールは、「同じ意味の情報の本体を1か所にまとめ、更新漏れや矛盾を防ぐ」ことであり、これはそのまま改ざん検知や監査のしやすさにもつながる。
正規化しすぎると JOIN だらけになり、開発・運用がつらくなるため、あえて一部を崩す「非正規化」も現場ではよく使われる。
非正規化は、「どれが本体で、どれがコピーか」を意識し、コピー側は多少のズレを許容する前提で行うのがポイント。
本人確認や権限判定など“セキュリティ上の真実”に関わる情報は、できるだけ正規化された本体に集約し、コピーを増やさない設計が望ましい。

ここまで来たあなたは、
「SQL を書く人」から一歩進んで、
「データの形そのものを設計する人」の入り口に立っています。
この感覚があると、今後のテーブル設計・API 設計・ログ設計が、全部一段クリアに見えるようになります。

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