ストーリー
「このテーブル、同じ住所が100行に重複してるぞ」
高橋アーキテクトがスキーマを見て眉をひそめた。住所を1箇所変更するために100行のUPDATEが走り、しかも一部が更新漏れで不整合を起こしていた。
「正規化は『重複をなくす技術』だ。理論を知れば、こういう問題は設計段階で防げる」
正規化とは
正規化(Normalization)は、データの冗長性を排除し、更新異常を防ぐためのテーブル分割技法。
更新異常の3つの種類
| 異常 | 説明 | 例 |
|---|---|---|
| 挿入異常 | 不要なデータなしに挿入できない | 注文なしに顧客情報を登録できない |
| 更新異常 | 冗長データの一部だけ更新される | 顧客住所が一部の行だけ変更される |
| 削除異常 | 必要なデータが一緒に消える | 最後の注文を消すと顧客情報も消える |
第一正規形(1NF)
条件: すべての属性が原子的(これ以上分解できない)な値を持つ
1NF違反の例
-- NG: 電話番号が複数値を持っている
CREATE TABLE customers_bad (
id INT PRIMARY KEY,
name VARCHAR(100),
phones VARCHAR(500) -- "090-1234-5678, 080-9876-5432"
);
-- OK: 電話番号を別テーブルに分離
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE customer_phones (
id INT PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id),
phone VARCHAR(20) NOT NULL,
phone_type VARCHAR(20) -- 'mobile', 'home', 'work'
);
TypeScriptでの表現
// NG: 配列をカンマ区切り文字列で保持
interface CustomerBad {
id: number;
name: string;
phones: string; // "090-xxxx, 080-xxxx"
}
// OK: 1NF準拠
interface Customer {
id: number;
name: string;
}
interface CustomerPhone {
id: number;
customerId: number;
phone: string;
phoneType: 'mobile' | 'home' | 'work';
}
第二正規形(2NF)
条件: 1NFを満たし、すべての非キー属性が主キー全体に完全関数従属する
複合主キーの場合に問題となる。主キーの一部にだけ依存する属性を分離する。
-- NG: 2NF違反(product_nameはproduct_idのみに依存)
CREATE TABLE order_items_bad (
order_id INT,
product_id INT,
product_name VARCHAR(200), -- product_idのみに依存(部分関数従属)
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- OK: 2NF準拠
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(200) NOT NULL
);
CREATE TABLE order_items (
order_id INT,
product_id INT REFERENCES products(id),
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
第三正規形(3NF)
条件: 2NFを満たし、非キー属性間の推移的関数従属がない
-- NG: 3NF違反(city_nameはzip_codeに依存し、zip_codeはidに依存 → 推移的依存)
CREATE TABLE customers_bad (
id INT PRIMARY KEY,
name VARCHAR(100),
zip_code VARCHAR(10),
city_name VARCHAR(100) -- zip_codeから決まる → 推移的関数従属
);
-- OK: 3NF準拠
CREATE TABLE zip_codes (
zip_code VARCHAR(10) PRIMARY KEY,
city_name VARCHAR(100) NOT NULL
);
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
zip_code VARCHAR(10) REFERENCES zip_codes(zip_code)
);
ボイス・コッド正規形(BCNF)
条件: すべての関数従属 X → Y において、Xが候補キーである
3NFとの違いは、非キー属性から候補キーの一部への関数従属も排除する点。
-- 例: 学生-科目-教員の関係
-- 制約: 1科目を担当する教員は1人、1教員は1科目のみ担当
-- {学生, 科目} → 教員 かつ 教員 → 科目
-- BCNF違反(教員 → 科目 だが、教員は候補キーでない)
CREATE TABLE enrollments_bad (
student_id INT,
subject VARCHAR(100),
teacher VARCHAR(100),
PRIMARY KEY (student_id, subject)
);
-- BCNF準拠: テーブルを分割
CREATE TABLE teacher_subjects (
teacher_id INT PRIMARY KEY,
subject VARCHAR(100) NOT NULL UNIQUE
);
CREATE TABLE enrollments (
student_id INT,
teacher_id INT REFERENCES teacher_subjects(teacher_id),
PRIMARY KEY (student_id, teacher_id)
);
非正規化の判断
正規化が常に正解ではない。以下の場合は意図的に非正規化する。
| シチュエーション | 理由 | 例 |
|---|---|---|
| 読み取り頻度が圧倒的に高い | JOINコスト削減 | レポートテーブル |
| 履歴を保持したい | 変更前の値を記録 | 注文時の商品名・価格 |
| 集計値のキャッシュ | リアルタイム計算のコスト | フォロワー数、いいね数 |
-- 意図的な非正規化: 注文時点の商品情報を保存
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT NOT NULL,
product_id INT NOT NULL,
product_name_snapshot VARCHAR(200) NOT NULL, -- 注文時の名前
unit_price_snapshot DECIMAL(10,2) NOT NULL, -- 注文時の価格
quantity INT NOT NULL
);
まとめ
| 正規形 | 条件 | 排除するもの |
|---|---|---|
| 1NF | 属性が原子的 | 繰り返しグループ |
| 2NF | 1NF + 完全関数従属 | 部分関数従属 |
| 3NF | 2NF + 推移的関数従属なし | 推移的関数従属 |
| BCNF | すべての決定子が候補キー | 非キーから候補キーへの依存 |
理解度チェックリスト
- 更新異常の3種類(挿入・更新・削除)を説明できる
- 1NF、2NF、3NFの条件をそれぞれ説明できる
- BCNFと3NFの違いを理解している
- 非正規化が適切なケースを判断できる
次のステップ
次はデータモデルを視覚的に表現するER図の読み書きを学ぶ。概念モデルから論理モデルへの変換技法を身につけよう。
推定読了時間: 25分