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正規形の条件:
- ✅ 各セルには単一の値のみ - カンマ区切りの値は禁止
- ✅ すべての行が一意 - 主キーで識別できる
- ✅ 列の順序に意味を持たせない - product_1, product_2みたいな命名はNG
- ✅ 各列のデータ型が統一 - 同じ列に数値と文字列が混在しない
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正規形を満たしている
- ✅ 部分関数従属がない
- 主キーが単一列の場合 → 自動的に第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正規形の定義 📚
以下の条件をすべて満たすテーブル:
- ✅ 第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正規形に違反している?
- どこが第2正規形に違反している?
- どこが第3正規形に違反している?
ステップ2:正規化する
- どのようにテーブルを分割すれば良い?
- 各テーブルの主キーは?
- 外部キーはどこに設定する?
ヒント💡
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正規形(1NF) - 繰り返し項目をなくす📋
- 1セルに1値
- product_1, product_2... → 行に分ける
-
第2正規形(2NF) - 部分関数従属をなくす🧩
- 主キーの一部で決まる属性を分離
- テーブルを分割
-
第3正規形(3NF) - 推移的関数従属をなくす🔄
- 主キー以外で決まる属性を分離
- さらにテーブルを分割
覚えておくこと💡
- ✅ 基本は第3正規形を目指す
- ✅ パフォーマンスとのトレードオフを理解
- ✅ 必要に応じて非正規化も検討
- ✅ まず正規化してから、測定して最適化!
10.2 実務での正規化🏢
設計フェーズ:
第3正規形を目指す📐
↓
運用開始:
パフォーマンス測定📊
↓
必要に応じて:
部分的に非正規化⚖️
10.3 次回予告🚀
次回はインデックスについて学びます!
「クエリが遅い...😰」
「データが増えたら重くなった...🐢」
こんな悩みを解決するインデックスの仕組みと使い方を、
初心者向けにわかりやすく解説します!
-- インデックスを張るだけで...
CREATE INDEX idx_email ON users(email);
-- クエリが100倍速くなることも!🚀
SELECT * FROM users WHERE email = 'tanaka@example.com';
お楽しみに!👋
データベース設計基礎シリーズ
- データベース設計の基本概念(ER図、エンティティ)
- SQLの基本をマスターしよう
- 正規化入門(第1〜第3正規形) ← 今回✅
- インデックスの仕組みと使い方 ← 次回
- トランザクションとACID特性
- 実践的なテーブル設計パターン集
- よくあるアンチパターンと対策
💬 質問や感想があれば、コメント欄でお気軽にどうぞ!
👍 役に立ったら、いいね&ストックをお願いします!
それでは、また次回!🙌