0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者向け】データベース設計の基礎 #3 - 正規化をマスターしよう!🎯

0
Last updated at Posted at 2026-02-17

1. はじめに

前回は、SQLの基本操作(CREATE TABLE、INSERT、SELECT、UPDATE、DELETE)を学びました。
テーブルを作って、データを操作できるようになりましたね!🎉

でも、「このテーブル設計で本当に大丈夫?🤔」と不安になることはありませんか?

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    customer_email VARCHAR(255),
    product_1 VARCHAR(100),
    price_1 DECIMAL(10,2),
    product_2 VARCHAR(100),
    price_2 DECIMAL(10,2)
    -- 商品が増えたらどうするの...?😰
);

今回学ぶ正規化(Normalization) は、
このような「設計の不安」を解消してくれる技術です!✨

2.1 前回のおさらい 📝

  • CREATE TABLE - テーブルを作成
  • INSERT - データを追加
  • SELECT - データを取得
  • UPDATE - データを更新
  • DELETE - データを削除

今回は、「良いテーブル設計」を学びます!

2. 正規化(Normalization)とは?

正規化とは、データベースのテーブル設計を最適化し、
データの重複を減らして、整合性を保つ技術です。

2.1 日常生活で例えると 🏠

📦 整理されていない部屋(非正規化)

想像してください...あなたの部屋が散らかっています。

テーブルの上:
- テレビのリモコン 3個(全部同じテレビ用!)
- 同じ本が 2冊
- 賞味期限切れの食品
- どこに何があるかわからない...😵

問題点:

  • 同じものが複数ある(無駄)💸
  • どれが最新か分からない😵
  • 情報が古くて使えない❌
  • 探すのに時間がかかる🕐

✨ 整理された部屋(正規化済み)

リビング: リモコン1個(テレビ用)
書斎: 本は1冊ずつ、きちんと本棚に
冷蔵庫: 食品は必要な分だけ、期限管理

メリット:

  • 無駄がない✅
  • どこに何があるか明確✅
  • 最新の状態を保てる✅
  • すぐに見つかる🔍

データベースの正規化も、これと同じ考え方です!

2.2 正規化の目的 🎯

目的 説明 絵文字
データの重複を減らす 同じ情報を何度も保存しない 💾✨
データの整合性を保つ 矛盾したデータを防ぐ 🔒
更新・削除・挿入の異常を防ぐ データ操作時の問題を回避 🛡️
ストレージを効率的に使う 容量の無駄を減らす 📦

2.3 正規化のレベル 📊

正規化には段階があります。

非正規形(0NF)
    ↓ 整理する
第1正規形(1NF) ← 繰り返し項目をなくす
    ↓ さらに整理
第2正規形(2NF) ← 部分的な依存をなくす
    ↓ もっと整理
第3正規形(3NF) ← 間接的な依存をなくす
    ↓
(さらに上のレベルもあるけど、実務では第3正規形まででOK!)

今回は、実務で最も重要な第3正規形までを学びます! 🎓

では、実際に見ていきましょう!👇

3. 非正規形テーブルの問題点

まずは、正規化されていないテーブル(非正規形)の問題を見てみましょう。

3.1 悪い例:注文管理テーブル ❌

ECサイトで、このようなテーブルを作ってしまったとします...

📋 orders テーブル(非正規形)

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    customer_email VARCHAR(255),
    customer_phone VARCHAR(20),
    product_1 VARCHAR(100),
    price_1 DECIMAL(10,2),
    product_2 VARCHAR(100),
    price_2 DECIMAL(10,2),
    product_3 VARCHAR(100),
    price_3 DECIMAL(10,2)
);

データの例

order_id customer_name customer_email product_1 price_1 product_2 price_2 product_3 price_3
1 田中太郎 tanaka@example.com ノートPC 80000 マウス 2000 NULL NULL
2 佐藤花子 sato@example.com キーボード 5000 NULL NULL NULL NULL
3 田中太郎 tanaka@example.com モニター 30000 HDMIケーブル 1000 スタンド 3000

一見、データが保存できているように見えますが...🤔

3.2 🔥 問題1:更新異常(Update Anomaly)

シナリオ:田中太郎さんのメールアドレスが変わった!

-- すべての注文レコードを更新しないといけない!😱
UPDATE orders 
SET customer_email = 'tanaka_new@example.com' 
WHERE customer_name = '田中太郎';

何が問題?

田中太郎さんの情報が2行に重複しています!

order_id=1: 田中太郎, tanaka@example.com
order_id=3: 田中太郎, tanaka@example.com

もし更新し忘れたら...

order_id customer_name customer_email
1 田中太郎 tanaka_new@example.com
3 田中太郎 tanaka@example.com ❌(更新し忘れ!)

同じ人なのに、メールアドレスが2つ存在!? 😵

これが更新異常です!

図解:更新異常 📊

┌─────────────────────────────────┐
│ 田中太郎さんの情報が2箇所に!    │
├─────────────────────────────────┤
│ order_id=1 → tanaka@...         │
│ order_id=3 → tanaka@...         │
└─────────────────────────────────┘
         ↓ メール変更
┌─────────────────────────────────┐
│ 片方だけ更新すると...             │
├─────────────────────────────────┤
│ order_id=1 → tanaka_new@... ✅  │
│ order_id=3 → tanaka@... ❌      │
└─────────────────────────────────┘
        データの不整合!💥

3.3 🔥 問題2:削除異常(Delete Anomaly)

シナリオ:佐藤花子さんの注文をキャンセル

DELETE FROM orders WHERE order_id = 2;

何が問題?

注文を削除すると、佐藤花子さんの顧客情報も消えてしまう! 😱

order_id customer_name customer_email
2 佐藤花子 sato@example.com

↓ DELETE実行

消えた!💨
「佐藤花子という顧客がいた」という記録も失われる...

これが削除異常です!

本来の期待動作

✅ 注文だけ削除したい
✅ 顧客情報は残したい

でも、非正規形だとこれができません!😢

3.4 🔥 問題3:挿入異常(Insert Anomaly)

シナリオ:新規顧客「鈴木一郎」さんを登録したい

でも、まだ注文がない...どうする?🤔

-- 無理やり登録しようとすると...
INSERT INTO orders (order_id, customer_name, customer_email) 
VALUES (4, '鈴木一郎', 'suzuki@example.com');
-- product_1, price_1 などがNULLになってしまう😓
order_id customer_name customer_email product_1 price_1
4 鈴木一郎 suzuki@example.com NULL NULL

何が問題?

  • 注文情報がないと顧客情報を登録できない 🚫
  • 無駄なNULL値だらけになる 😓
  • データの意味が不明確(注文なのに商品がない?)🤔

これが挿入異常です!

3.5 🔥 問題4:柔軟性がない 📏

Q: 商品を4つ以上注文したい場合は?
A: テーブル構造を変更しないといけない!
   ALTER TABLE orders ADD COLUMN product_4 VARCHAR(100);
   ALTER TABLE orders ADD COLUMN price_4 DECIMAL(10,2);
   😱 大変!

Q: 商品の詳細情報(在庫数、説明など)を管理したい場合は?
A: またカラムを追加...どんどん横に長くなる...
   📏📏📏📏📏📏📏📏

3.6 😱 まとめ:非正規形の3大異常

異常の種類 問題内容
更新異常 📝📝📝 重複データの更新漏れで不整合が発生 田中さんのメール変更で片方だけ更新
削除異常 🗑️💔 削除したくないデータまで消える 注文削除で顧客情報も消失
挿入異常 🚫😓 不要なNULLを入れないと登録できない 顧客だけ登録できない

3.7 なぜこうなる? 🤔

原因:

  • 異なる種類のデータ(顧客情報と注文情報と商品情報)が1つのテーブルに混在
  • データの重複
  • テーブル構造が現実を正しく表現できていない

解決策:

  • 正規化!

これらを解決するのが、次に学ぶ正規化です!👇

4. 第1正規形(1NF: First Normal Form)

最初のステップは、繰り返し項目を排除することです!📋

4.1 ルール:繰り返し項目をなくす 🎯

第1正規形の条件:

  1. 各セルには単一の値のみ - カンマ区切りの値は禁止
  2. すべての行が一意 - 主キーで識別できる
  3. 列の順序に意味を持たせない - product_1, product_2みたいな命名はNG
  4. 各列のデータ型が統一 - 同じ列に数値と文字列が混在しない

4.2 ❌ 悪い例1:カンマ区切りの値

CREATE TABLE orders_bad1 (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    products VARCHAR(500)  -- ←これがダメ!
);
order_id customer_name products
1 田中太郎 ノートPC, マウス, キーボード
2 佐藤花子 モニター

何が問題? 🤔

-- 「ノートPC」を含む注文を検索したい
SELECT * FROM orders_bad1 
WHERE products LIKE '%ノートPC%';

一見動きそうですが...

  • 「ノートPC用バッグ」も引っかかる😱
  • 検索が遅い🐢
  • 商品ごとの集計ができない📊
  • 商品の追加・削除が面倒😓

4.3 ❌ 悪い例2:繰り返し列

CREATE TABLE orders_bad2 (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    product_1 VARCHAR(100),  -- ←これがダメ!
    product_2 VARCHAR(100),  -- ←これもダメ!
    product_3 VARCHAR(100)   -- ←これもダメ!
);
order_id customer_name product_1 product_2 product_3
1 田中太郎 ノートPC マウス NULL
2 佐藤花子 キーボード NULL NULL

何が問題? 🤔

  • 商品が4つ以上だとどうする?📏
  • NULLだらけで無駄💾
  • 柔軟性がない🚫

4.4 ✅ 良い例:第1正規形

方法:行を分ける!

CREATE TABLE orders_1nf (
    order_id INT,
    customer_name VARCHAR(50),
    customer_email VARCHAR(255),
    product_name VARCHAR(100),
    price DECIMAL(10,2),
    PRIMARY KEY (order_id, product_name)  -- 複合主キー
);

データの例

INSERT INTO orders_1nf VALUES
(1, '田中太郎', 'tanaka@example.com', 'ノートPC', 80000),
(1, '田中太郎', 'tanaka@example.com', 'マウス', 2000),
(2, '佐藤花子', 'sato@example.com', 'キーボード', 5000),
(3, '田中太郎', 'tanaka@example.com', 'モニター', 30000);
order_id customer_name customer_email product_name price
1 田中太郎 tanaka@example.com ノートPC 80000
1 田中太郎 tanaka@example.com マウス 2000
2 佐藤花子 sato@example.com キーボード 5000
3 田中太郎 tanaka@example.com モニター 30000

これで繰り返し項目がなくなりました! 🎉

4.5 🎯 第1正規形にすると...

できるようになること ✨

  • 特定の商品を含む注文を簡単に検索!
SELECT order_id, customer_name 
FROM orders_1nf 
WHERE product_name = 'ノートPC';
order_id customer_name
1 田中太郎
  • 注文ごとの合計金額を計算!
SELECT 
    order_id, 
    customer_name, 
    SUM(price) AS total_amount
FROM orders_1nf
GROUP BY order_id, customer_name;
order_id customer_name total_amount
1 田中太郎 82000
2 佐藤花子 5000
3 田中太郎 30000
  • 商品ごとの販売数を集計!
SELECT 
    product_name, 
    COUNT(*) AS sold_count
FROM orders_1nf
GROUP BY product_name;
product_name sold_count
ノートPC 1
マウス 1
キーボード 1
モニター 1

便利! 😊

まだ残っている問題 ⚠️

田中太郎さんの情報(customer_name, customer_email)が
複数行に重複している...
→ これは第2正規形で解決します!

4.6 チェックリスト ✅

あなたのテーブルは第1正規形ですか?

  • 繰り返し項目(product_1, product_2...)がない
  • カンマ区切りの値(「A,B,C」みたいな)がない
  • すべての行に主キーがある
  • 1つのセルに1つの値のみ

すべてチェックできたら、第2正規形へ進みましょう!👉

5. 第2正規形(2NF: Second Normal Form)

次のステップは、部分関数従属を排除することです!

「部分関数従属って何...?🤔」

難しい言葉ですが、超簡単に言うと:
「主キーの一部だけで決まる情報を、別テーブルに分ける」 ということです!

5.1 まず「関数従属」を理解しよう 🔗

関数従属とは?

「AがわかればBがわかる」という関係のことです。

日常の例 🏫

学生証番号 → 学生の名前、学年、クラス
(学生証番号がわかれば、その人の情報がわかる)

商品コード → 商品名、価格
(商品コードがわかれば、商品の詳細がわかる)

郵便番号 → 都道府県、市区町村
(郵便番号がわかれば、住所の一部がわかる)

記号で表すと

        A          →         B
        ↑                    ↑
決定項(これがわかれば)  従属項(これがわかる)

データベースの例

user_id → username, email, age
(user_idがわかれば、usernameとemailとageがわかる)

product_id → product_name, price
(product_idがわかれば、product_nameとpriceがわかる)

5.2 部分関数従属とは? 🧩

複合主キーの一部だけで決まる属性のことです。

第1正規形のテーブルを見てみましょう

CREATE TABLE orders_1nf (
    order_id INT,              -- 主キーの一部①
    product_name VARCHAR(100), -- 主キーの一部②
    customer_name VARCHAR(50),
    customer_email VARCHAR(255),
    price DECIMAL(10,2),
    PRIMARY KEY (order_id, product_name)  -- 複合主キー
);
order_id product_name customer_name customer_email price
1 ノートPC 田中太郎 tanaka@example.com 80000
1 マウス 田中太郎 tanaka@example.com 2000
2 キーボード 佐藤花子 sato@example.com 5000

🔍 依存関係を分析してみよう

主キーは (order_id, product_name) の組み合わせです。

では、各カラムは何で決まるでしょうか?

customer_name は order_id だけで決まる! ← 部分関数従属⚠️
customer_email は order_id だけで決まる! ← 部分関数従属⚠️
price は product_name だけで決まる!      ← 部分関数従属⚠️

図解:部分関数従属 📊

複合主キー: (order_id, product_name)
            ↓         ↓
    order_id だけ  product_name だけ
         ↓               ↓
   customer_name      price
   customer_email
   
これが「部分」関数従属!
(主キーの「一部」だけで決まっている)

問題点:

  • 田中太郎さんの情報が2行に重複 📝📝
  • 商品の価格が重複 💰💰
  • 更新異常が起こる!😱

5.3 第2正規形への変換 🔧

解決策:テーブルを分割する!

✅ 変換後:3つのテーブルに分ける

1️⃣ 注文テーブル(orders)

「order_id で決まる情報」だけを集める

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    customer_email VARCHAR(255),
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO orders (order_id, customer_name, customer_email) VALUES
(1, '田中太郎', 'tanaka@example.com'),
(2, '佐藤花子', 'sato@example.com'),
(3, '田中太郎', 'tanaka@example.com');
order_id customer_name customer_email
1 田中太郎 tanaka@example.com
2 佐藤花子 sato@example.com
3 田中太郎 tanaka@example.com

依存関係:

order_id → customer_name, customer_email

2️⃣ 商品テーブル(products)

「product_name で決まる情報」だけを集める

CREATE TABLE products (
    product_id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(100) NOT NULL UNIQUE,
    price DECIMAL(10,2) NOT NULL
);
INSERT INTO products (product_name, price) VALUES
('ノートPC', 80000),
('マウス', 2000),
('キーボード', 5000),
('モニター', 30000);
product_id product_name price
1 ノートPC 80000
2 マウス 2000
3 キーボード 5000
4 モニター 30000

依存関係:

product_id → product_name, price

3️⃣ 注文明細テーブル(order_items)

「どの注文に、どの商品が含まれるか」だけを管理

CREATE TABLE order_items (
    order_item_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT NOT NULL DEFAULT 1,
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);
INSERT INTO order_items (order_id, product_id, quantity) VALUES
(1, 1, 1),  -- 注文1:ノートPC
(1, 2, 1),  -- 注文1:マウス
(2, 3, 1),  -- 注文2:キーボード
(3, 4, 1);  -- 注文3:モニター
order_item_id order_id product_id quantity
1 1 1 1
2 1 2 1
3 2 3 1
4 3 4 1

依存関係:

order_item_id → order_id, product_id, quantity

5.4 🎉 第2正規形にすると何が変わる?

Before(第1正規形)❌

-- 田中太郎さんのメールアドレス変更
UPDATE orders_1nf 
SET customer_email = 'tanaka_new@example.com'
WHERE customer_name = '田中太郎';  -- 複数行更新される😱

問題:

  • 田中さんの注文が2件あるから、2行更新が必要
  • 1行だけ更新し忘れると不整合(更新異常)😵

After(第2正規形)✅

-- 1回の更新で完了!🎉
UPDATE orders 
SET customer_email = 'tanaka_new@example.com'
WHERE order_id = 1;

メリット:

  • 1箇所更新すればOK!✨
  • 更新漏れのリスクなし✅

商品価格の変更も簡単!

-- ノートPCの価格を変更
UPDATE products 
SET price = 75000 
WHERE product_name = 'ノートPC';

1回で完了! 第1正規形だと、複数の注文行を更新する必要がありました😱

5.5 データの取得方法 🔍

「テーブルが分かれたら、データを取るのが大変では?🤔」

JOIN(結合)を使います!

-- 注文1の詳細を表示
SELECT 
    o.order_id,
    o.customer_name,
    p.product_name,
    p.price,
    oi.quantity
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE o.order_id = 1;
order_id customer_name product_name price quantity
1 田中太郎 ノートPC 80000 1
1 田中太郎 マウス 2000 1

元のデータと同じように取得できます!

5.6 第2正規形の定義 📚

以下の条件をすべて満たすテーブル:

  1. 第1正規形を満たしている
  2. 部分関数従属がない
    • 主キーが単一列の場合 → 自動的に第2正規形
    • 主キーが複合の場合 → すべての非キー属性が主キー全体に依存

チェックリスト ✅
あなたのテーブルは第2正規形ですか?

  • 第1正規形を満たしている
  • 主キーが単一列、または
  • 主キーが複合でも、すべての属性が主キー全体に依存している
  • 主キーの一部だけで決まる属性がない

すべてチェックできたら、第3正規形へ!👉


6. 第3正規形(3NF: Third Normal Form)

最後のステップは、推移的関数従属を排除することです!

「推移的関数従属...また難しい言葉が!😵」

大丈夫、超簡単に言うと:
「主キー → A → B のように、間接的に決まる情報を別テーブルに分ける」 ということです!

6.1 推移的関数従属とは? 🔄

「A → B → C」のように、間接的に依存している関係のことです。

日常の例 🏢

社員ID → 部署ID → 部署名
 ↓        ↓        ↓
 A    →   B    →   C

社員IDがわかる
  ↓
部署IDがわかる
  ↓
部署名がわかる

(間接的!)

もっと身近な例 🏠

あなた → あなたの親 → 親の出身地

あなたがわかる
  ↓
あなたの親がわかる
  ↓
親の出身地がわかる

(あなたから「直接」親の出身地はわからない)

例:現在のordersテーブルを見てみよう

第2正規形までは達成しましたが、まだ問題が残っています!

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    customer_email VARCHAR(255),
    customer_phone VARCHAR(20),
    customer_address VARCHAR(255),
    order_date TIMESTAMP
);

データの例

INSERT INTO orders VALUES
(1, '田中太郎', 'tanaka@example.com', '090-1234-5678', '東京都渋谷区...', '2026-01-15'),
(2, '佐藤花子', 'sato@example.com', '080-9876-5432', '大阪府大阪市...', '2026-01-16'),
(3, '田中太郎', 'tanaka@example.com', '090-1234-5678', '東京都渋谷区...', '2026-01-20');
order_id customer_name customer_email customer_phone customer_address
1 田中太郎 tanaka@example.com 090-1234-5678 東京都渋谷区...
2 佐藤花子 sato@example.com 080-9876-5432 大阪府大阪市...
3 田中太郎 tanaka@example.com 090-1234-5678 東京都渋谷区...

🔍 依存関係を分析してみよう

order_id → customer_name ... ✅ (直接依存)
order_id → customer_email, customer_phone, customer_address ... ✅

でも実は...
customer_name → customer_email, customer_phone, customer_address 🤔

これが推移的関数従属!

order_id → customer_name → customer_email
   A    →       B       →        C
   
(間接的に決まっている!)

図解:推移的関数従属 📊

┌──────────┐
│ order_id │ (主キー)
└─────┬────┘
      │ 直接依存
      ↓
┌─────────────┐
│customer_name│
└──────┬──────┘
       │ これも依存!
       ↓
┌──────────────────────┐
│ customer_email       │
│ customer_phone       │
│ customer_address     │
└──────────────────────┘

order_id から見ると「間接的」!

問題点 ⚠️

田中太郎さんが2回注文している
→ 顧客情報が重複している!📝📝

もし田中太郎さんが引っ越したら?
→ order_id=1 と order_id=3 の両方を更新しないといけない😱

これは第2正規形でも解決できていない問題です!

6.2 第3正規形への変換 🔧

解決策:顧客情報を別テーブルに分離!

✅ 変換後:顧客テーブルを分離

1️⃣ 顧客テーブル(customers)

CREATE TABLE customers (
    customer_id INT AUTO_INCREMENT PRIMARY KEY,
    customer_name VARCHAR(50) NOT NULL,
    customer_email VARCHAR(255) NOT NULL UNIQUE,
    customer_phone VARCHAR(20),
    customer_address VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO customers (customer_name, customer_email, customer_phone, customer_address) VALUES
('田中太郎', 'tanaka@example.com', '090-1234-5678', '東京都渋谷区...'),
('佐藤花子', 'sato@example.com', '080-9876-5432', '大阪府大阪市...');
customer_id customer_name customer_email customer_phone customer_address
1 田中太郎 tanaka@example.com 090-1234-5678 東京都渋谷区...
2 佐藤花子 sato@example.com 080-9876-5432 大阪府大阪市...

依存関係:

customer_id → customer_name, customer_email, customer_phone, customer_address
(顧客IDがわかれば、すべての顧客情報がわかる)

2️⃣ 注文テーブル(orders)を修正

CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT NOT NULL,  -- 顧客IDのみ参照✨
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    total_amount DECIMAL(10,2),
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
INSERT INTO orders (order_id, customer_id, order_date, total_amount) VALUES
(1, 1, '2026-01-15', 82000),
(2, 2, '2026-01-16', 5000),
(3, 1, '2026-01-20', 34000);
order_id customer_id order_date total_amount
1 1 2026-01-15 82000
2 2 2026-01-16 5000
3 1 2026-01-20 34000

依存関係:

order_id → customer_id, order_date, total_amount
(注文IDがわかれば、顧客ID・注文日・合計金額がわかる)

ポイント:

  • 顧客情報は customer_id だけで参照!
  • 詳細は customers テーブルから取得!

6.3🎉 第3正規形にすると何が変わる?

Before(第2正規形)❌

-- 田中太郎さんの住所変更
UPDATE orders 
SET customer_address = '神奈川県横浜市...'
WHERE customer_name = '田中太郎';  -- 複数の注文を更新😱
問題:

田中太郎さんが10回注文していたら、10行更新が必要😱
更新漏れのリスク⚠️

After(第3正規形)✅

-- 1回の更新で完了!🎉
UPDATE customers 
SET customer_address = '神奈川県横浜市...'
WHERE customer_id = 1;

メリット:

  • 顧客情報は customers テーブルに1回だけ✨
  • 何回注文しても、顧客情報は重複しない👍
  • 注文を全部削除しても、顧客情報は残る💾

注文を削除しても顧客情報は残る!

-- 注文3を削除
DELETE FROM orders WHERE order_id = 3;

第3正規形:

  • 注文だけ削除される✅
  • 田中太郎さんの顧客情報は残る✅

非正規形:

  • 注文と一緒に顧客情報も消える💔(削除異常)

データの取得方法 🔍

-- 注文1の詳細を表示(顧客情報も含めて)
SELECT 
    o.order_id,
    c.customer_name,
    c.customer_email,
    o.order_date,
    o.total_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_id = 1;
order_id customer_name customer_email order_date total_amount
1 田中太郎 tanaka@example.com 2026-01-15 82000

JOINで簡単に取得できます! ✨

6.4 完成形:第3正規形のER図 🎨

美しい! すべてのテーブルが適切に分離されています!✨

6.5 第3正規形の定義 📚

以下の条件をすべて満たすテーブル:

  1. 第2正規形を満たしている
  2. 推移的関数従属がない
    • すべての非キー属性が、主キーに直接依存している
    • 非キー属性同士に依存関係がない

簡単に言うと

「主キー以外の情報で、他の情報が決まる」がない!

チェックリスト ✅

あなたのテーブルは第3正規形ですか?

  • 第2正規形を満たしている
  • 非キー属性が主キーに直接依存している
  • 非キー属性同士に依存関係がない
  • 「A → B → C」のような間接的な依存がない

すべてチェックできたら、正規化マスター!🎓

7. 正規化の3ステップまとめ

お疲れ様でした!
3つの正規形を学びましたね!🎉
ここで、全体を振り返ってみましょう!

7.1 🎯 各正規形のポイント

正規形 やること キーワード 解決する問題 絵文字
第1正規形 繰り返し項目を排除 1セルに1値 product_1, product_2... 📋
第2正規形 部分関数従属を排除 テーブル分割 主キーの一部で決まる属性 🧩
第3正規形 推移的関数従属を排除 さらに分割 主キー以外で決まる属性 🔄

7.2 ビフォー・アフター比較 📸

❌ 非正規形(すべて1つのテーブル)

orders_all_in_one テーブル
├─ order_id (PK)
├─ customer_name ──┐
├─ customer_email  ├─ 顧客情報が重複!📝📝
├─ customer_phone  │
├─ customer_address┘
├─ product_1 ──┐
├─ price_1     ├─ 繰り返し項目!📏📏
├─ product_2   │
├─ price_2     │
├─ product_3   │
└─ price_3 ────┘

問題:

  • 更新異常😱:田中さんのメール変更で複数行更新
  • 削除異常💔:注文削除で顧客情報も消える
  • 挿入異常🚫:顧客だけ登録できない
  • 柔軟性がない📏:商品数が固定

✅ 第3正規形(適切に分割)

customers (顧客) ─────┐
├─ customer_id (PK)   
├─ customer_name      
├─ customer_email     
└─ customer_address   
                      
orders (注文) ────────┐
├─ order_id (PK)      
├─ customer_id (FK) 
└─ order_date         
                      
products (商品) ──────┐
├─ product_id (PK)    
├─ product_name       
└─ price              
                      
order_items (注文明細)─┐
├─ order_item_id (PK) 
├─ order_id (FK) 
├─ product_id (FK) 
└─ quantity

メリット:

  • データの重複なし✨
  • 整合性が保たれる🔒
  • 更新が簡単✅
  • 柔軟に拡張できる🚀

7.3 覚え方のコツ 💡

方法1:キーワードで覚える

第1正規形:「繰り返しをなくす」📋
    ↓ product_1, product_2... を行に分ける
    
第2正規形:「部分をなくす」🧩
    ↓ 主キーの一部で決まる属性を分離
    
第3正規形:「間接をなくす」🔄
    ↓ 主キー以外で決まる属性を分離

方法2:段階で覚える

1NF: 各セルに1つの値
2NF: 主キー全体に依存
3NF: 主キーに直接依存

方法3:質問で覚える

1NF:「このセル、カンマ区切りになってない?」
2NF:「主キーの一部だけで決まる属性ない?」
3NF:「主キー以外の属性で決まる属性ない?」

7.4 実践のヒント 🎯

ステップ1:まずエンティティを洗い出す

顧客、商品、注文、注文明細...

ステップ2:各エンティティの属性を決める

顧客:ID、名前、メール、住所...
商品:ID、名前、価格...

ステップ3:依存関係を確認

customer_id → customer_name, email...
product_id → product_name, price...

ステップ4:正規化をチェック

✅ 繰り返し項目はない?
✅ 部分関数従属はない?
✅ 推移的関数従属はない?

7.5 クイズ 📝

次のテーブルは第何正規形?

問題1

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(50),
    skills VARCHAR(500)  -- 'Java,Python,SQL' のように格納
);
答え👇

非正規形(0NF)
理由:skills カラムにカンマ区切りの値が入っている。
第1正規形の 「1セルに1値」 の原則に違反!

問題2

sqlCREATE TABLE orders (
    order_id INT,
    product_name VARCHAR(100),
    customer_name VARCHAR(50),
    customer_email VARCHAR(255),
    price DECIMAL(10,2),
    PRIMARY KEY (order_id, product_name)
);
答え👇

第1正規形(1NF)

理由:

  • ✅ 繰り返し項目はない
  • ❌ customer_name, customer_emailは order_id だけで決まる(部分関数従属)
  • ❌ price は product_name だけで決まる(部分関数従属)

第2正規形にするには、テーブルを分割する必要がある!

問題3

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(50),
    dept_id INT,
    dept_name VARCHAR(50),  -- dept_id で決まる!
    salary DECIMAL(10,2)
);
答え👇

第2正規形(2NF)

理由:

  • ✅ 第1正規形を満たしている
  • ✅ 主キーは単一列なので、部分関数従属はない
  • ❌ dept_name は dept_id で決まる(推移的関数従属)

第3正規形にするには、部署情報を別テーブルに分離!

全問正解できましたか?🎉


8. 正規化のデメリットと非正規化 🤔

「正規化は良いことばかり!」...と言いたいところですが、
実はやりすぎると問題もあります。

8.1 デメリット:パフォーマンスの低下 🐌

第3正規形でデータを取得する場合

  • 注文の詳細を表示するには、4つのテーブルをJOINする必要がある
SELECT 
    c.customer_name,
    c.customer_email,
    o.order_date,
    p.product_name,
    oi.quantity,
    p.price
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE o.order_id = 1;

問題点:

  • JOINが多くなると、処理が遅くなる🐢
  • クエリが複雑になる😵
  • インデックスを適切に設定しないと、さらに遅い...⏱️

非正規形なら...

-- 1つのテーブルから取得できる(速い!)
SELECT * FROM orders_all_in_one WHERE order_id = 1;

速いけど:

  • データの整合性リスク⚠️
  • 更新が大変😱

8.2 非正規化(Denormalization)とは? 📈

意図的に正規化を崩して、パフォーマンスを上げる手法です。

例1:注文の合計金額を保存

✅ 正規化された設計

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date TIMESTAMP
    -- total_amount は保存しない(order_itemsから計算)
);

クエリ:

-- 毎回計算が必要(遅い🐢)
SELECT 
    o.order_id,
    SUM(oi.quantity * oi.unit_price) AS total_amount
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY o.order_id;

📊 非正規化した設計

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date TIMESTAMP,
    total_amount DECIMAL(10,2)  -- 計算結果を保存!✨
);

クエリ:

-- 即座に取得できる!(速い🚀)
SELECT order_id, total_amount FROM orders;

トレードオフ:

  • ✅ 読み取りが速い🚀
  • ❌ 更新時に total_amount も更新が必要🔧
  • ❌ データの整合性を保つ仕組みが必要

例2:顧客の最終注文日を保存

✅ 正規化された設計

CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    customer_name VARCHAR(50)
    -- last_order_date は保存しない
);
-- 最終注文日を取得(遅い🐢)
SELECT 
    c.customer_id,
    c.customer_name,
    MAX(o.order_date) AS last_order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name;

📊 非正規化した設計

CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    last_order_date DATE  -- 非正規化!✨
);

即座に取得!(速い🚀)

SELECT customer_id, customer_name, last_order_date 
FROM customers;

整合性を保つ方法:

-- 注文が追加されたら、customersテーブルも更新
INSERT INTO orders (customer_id, order_date) VALUES (1, CURRENT_DATE);
UPDATE customers SET last_order_date = CURRENT_DATE WHERE customer_id = 1;

または、トリガーを使う:

CREATE TRIGGER update_last_order
AFTER INSERT ON orders
FOR EACH ROW
UPDATE customers 
SET last_order_date = NEW.order_date 
WHERE customer_id = NEW.customer_id;

8.3 いつ非正規化するべき? 🤷

正規化を優先すべき場合✅

  • データの整合性が最重要
  • 更新・削除が頻繁
  • ストレージが限られている
  • システムの初期段階
  • チームの技術力がまだ低い

非正規化を検討すべき場合📊

  • 読み取りが圧倒的に多い(読み:書き = 100:1 など)
  • パフォーマンスのボトルネックが明確
  • 整合性を保つ仕組みを構築できる
  • アクセス数が多いサービス
  • レポート機能など集計が多い

8.4 ベストプラクティス💡

1. まず第3正規形で設計する📐
     ↓
2. 実装して運用開始🚀
     ↓
3. パフォーマンス測定📊
     ↓
4. ボトルネックを特定🔍
     ↓
5. 必要なら部分的に非正規化⚖️
     ↓
6. 整合性を保つ仕組み(トリガー、アプリケーション)を用意🔒

基本は正規化、必要に応じて非正規化! 🎯

実例:大手サービスでの非正規化 🌐

Twitter風SNSの例

-- 正規化
CREATE TABLE posts (
    post_id INT PRIMARY KEY,
    user_id INT,
    content TEXT
);

-- フォロワー数は都度カウント(遅い)
SELECT COUNT(*) FROM follows WHERE following_id = 1;

-- 非正規化:フォロワー数を保存
CREATE TABLE users (
    user_id INT PRIMARY KEY,
    username VARCHAR(50),
    follower_count INT  -- 非正規化!
);

-- 即座に表示!(速い)
SELECT username, follower_count FROM users;

ECサイトの例

-- 商品の平均評価を毎回計算(遅い)
SELECT AVG(rating) FROM reviews WHERE product_id = 100;

-- 非正規化:平均評価を保存
CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(100),
    average_rating DECIMAL(3,2),  -- 非正規化!
    review_count INT              -- 非正規化!
);

大規模サービスでは、パフォーマンスのために部分的に非正規化することが多い! 🚀


9. 実践演習:あなたもやってみよう!💪

学んだことを使って、実際のシナリオに挑戦しましょう!

9.1 問題:このテーブルを第3正規形にしてください

📋 社員管理テーブル(employees)

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(50),
    dept_id INT,
    dept_name VARCHAR(50),
    dept_location VARCHAR(50),
    project_1 VARCHAR(100),
    project_2 VARCHAR(100),
    salary DECIMAL(10,2)
);

データの例

emp_id emp_name dept_id dept_name dept_location project_1 project_2 salary
1 田中太郎 10 営業部 東京 案件A 案件B 400000
2 佐藤花子 20 開発部 大阪 案件C NULL 500000
3 鈴木一郎 10 営業部 東京 案件A NULL 380000

🤔 考えてみよう!

ステップ1:問題点を洗い出す

  1. どこが第1正規形に違反している?
  2. どこが第2正規形に違反している?
  3. どこが第3正規形に違反している?

ステップ2:正規化する

  1. どのようにテーブルを分割すれば良い?
  2. 各テーブルの主キーは?
  3. 外部キーはどこに設定する?

ヒント💡

project_1, project_2 → 繰り返し項目!(第1正規形違反)
dept_name, dept_location → dept_id で決まる!(推移的関数従属)

📊 解答例(クリックで表示)

解答を見る👇
  • ✅ 第3正規形への変換
  • 1️⃣ 部署テーブル(departments)
CREATE TABLE departments (
    dept_id INT PRIMARY KEY,
    dept_name VARCHAR(50) NOT NULL UNIQUE,
    dept_location VARCHAR(50)
);

INSERT INTO departments VALUES
(10, '営業部', '東京'),
(20, '開発部', '大阪'),
(30, '人事部', '東京');
dept_id dept_name dept_location
10 営業部 東京
20 開発部 大阪
30 営業部 東京

2️⃣ 社員テーブル(employees)

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(50) NOT NULL,
    dept_id INT NOT NULL,
    salary DECIMAL(10,2),
    FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
INSERT INTO employees VALUES
(1, '田中太郎', 10, 400000),
(2, '佐藤花子', 20, 500000),
(3, '鈴木一郎', 10, 380000);
emp_id emp_name dept_id salary
1 田中太郎 10 400000
2 佐藤花子 20 500000
3 鈴木一郎 10 380000

3️⃣ プロジェクトテーブル(projects)

CREATE TABLE projects (
    project_id INT AUTO_INCREMENT PRIMARY KEY,
    project_name VARCHAR(100) NOT NULL UNIQUE
);
INSERT INTO projects (project_name) VALUES
('案件A'),
('案件B'),
('案件C');
project_id project_name
1 案件A
2 案件B
3 案件C

4️⃣ 社員-プロジェクト関連テーブル(employee_projects)

CREATE TABLE employee_projects (
    emp_id INT,
    project_id INT,
    PRIMARY KEY (emp_id, project_id),
    FOREIGN KEY (emp_id) REFERENCES employees(emp_id),
    FOREIGN KEY (project_id) REFERENCES projects(project_id)
);
INSERT INTO employee_projects VALUES
(1, 1),  -- 田中太郎:案件A
(1, 2),  -- 田中太郎:案件B
(2, 3),  -- 佐藤花子:案件C
(3, 1);  -- 鈴木一郎:案件A
emp_id project_id
1 1
1 2
2 3
3 1

🎉 これで第3正規形!

  • 繰り返し項目がなくなった✅(project_1, project_2 → 別テーブル)
  • 部署情報の重複がなくなった✅(departments テーブルに分離)
  • 推移的関数従属がなくなった✅(dept_id → dept_name, dept_location)
  • 社員は複数のプロジェクトに参加できる✅(柔軟性)

データの取得例:

-- 社員と所属部署を取得
SELECT 
    e.emp_name,
    d.dept_name,
    d.dept_location
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
-- 社員とプロジェクトを取得
SELECT 
    e.emp_name,
    p.project_name
FROM employees e
INNER JOIN employee_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id;

できましたか?🎉
これが正規化の実践です!

10. まとめ

お疲れ様でした!🎉
正規化について、しっかり学べましたね!

10.1 今回学んだこと📚

正規化とは

  • データの重複を減らし、整合性を保つ技術✨
  • 更新異常・削除異常・挿入異常を防ぐ🛡️

正規化の3ステップ

  1. 第1正規形(1NF) - 繰り返し項目をなくす📋

    • 1セルに1値
    • product_1, product_2... → 行に分ける
  2. 第2正規形(2NF) - 部分関数従属をなくす🧩

    • 主キーの一部で決まる属性を分離
    • テーブルを分割
  3. 第3正規形(3NF) - 推移的関数従属をなくす🔄

    • 主キー以外で決まる属性を分離
    • さらにテーブルを分割

覚えておくこと💡

  • ✅ 基本は第3正規形を目指す
  • ✅ パフォーマンスとのトレードオフを理解
  • ✅ 必要に応じて非正規化も検討
  • ✅ まず正規化してから、測定して最適化!

10.2 実務での正規化🏢

設計フェーズ:
  第3正規形を目指す📐
      ↓
運用開始:
  パフォーマンス測定📊
      ↓
必要に応じて:
  部分的に非正規化⚖️

10.3 次回予告🚀

次回はインデックスについて学びます!

「クエリが遅い...😰」
「データが増えたら重くなった...🐢」

こんな悩みを解決するインデックスの仕組みと使い方を、
初心者向けにわかりやすく解説します!

-- インデックスを張るだけで...
CREATE INDEX idx_email ON users(email);

-- クエリが100倍速くなることも!🚀
SELECT * FROM users WHERE email = 'tanaka@example.com';

お楽しみに!👋


データベース設計基礎シリーズ

  1. データベース設計の基本概念(ER図、エンティティ)
  2. SQLの基本をマスターしよう
  3. 正規化入門(第1〜第3正規形) ← 今回✅
  4. インデックスの仕組みと使い方 ← 次回
  5. トランザクションとACID特性
  6. 実践的なテーブル設計パターン集
  7. よくあるアンチパターンと対策

💬 質問や感想があれば、コメント欄でお気軽にどうぞ!
👍 役に立ったら、いいね&ストックをお願いします!

それでは、また次回!🙌

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?