こんにちは、かつコーチです。
これまでCREATE TABLE、データ型、主キー・外部キーとテーブル設計の要素技術を見てきました。
今回はそれらを踏まえて、テーブル設計の理論的な支柱である正規化を扱います。
この記事は前提知識ありきの上級者向け内容として、正規化の考え方とメリット・デメリットを実践目線で整理します。
正規化とは何を目指すプロセスか
正規化とは、データの重複や更新時の不整合を排除するために、テーブルを段階的に分割していく設計手法です。
正規化には第1正規形から第5正規形、さらにボイス・コッド正規形まで複数の段階が定義されていますが、実務で意識されるのはほぼ第3正規形までです。
正規化の本質は「1つの事実は1箇所にだけ存在させる」という原則にあります。
これが崩れると、同じ情報を複数箇所で更新する必要が生じ、更新漏れによるデータの不整合が発生します。
第1正規形:繰り返し項目の排除
第1正規形は、1つのセルに複数の値を持たせないという制約です。
以下は正規化前の注文テーブルです。
-- ❌ 正規化前:1つのカラムに複数商品をカンマ区切りで格納
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
member_id INT NOT NULL,
product_names VARCHAR(255) -- '商品A,商品B,商品C'
);
product_namesにカンマ区切りで複数商品を詰め込む設計は、一見シンプルに見えます。
しかし実際には「特定の商品を含む注文を検索する」クエリがLIKE '%商品B%'のような非効率なものになり、インデックスも効きません。
第1正規形を満たすには、繰り返す項目を別テーブルに切り出します。
-- ✅ 第1正規形:繰り返し項目を別テーブルへ分離
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
member_id INT NOT NULL
);
CREATE TABLE order_items (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
product_name VARCHAR(100) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
これで1商品1レコードとなり、WHERE product_name = '商品B'のような検索が可能になります。
第2正規形:部分関数従属の排除
第2正規形は、複合主キーを持つテーブルにおいて、主キーの一部だけに従属する列を排除するというルールです。
「複合主キーではない」テーブルは自動的に第2正規形を満たすため、複合主キーを持つケースでのみ意識が必要になります。
-- ❌ 正規化前:product_nameがproduct_idだけに従属している
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
product_name VARCHAR(100) NOT NULL, -- product_idだけで決まる
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
product_nameはproduct_idさえ分かれば決まる値で、複合主キー(order_id, product_id)の一部(product_id)にしか従属していません。
これは部分関数従属と呼ばれ、第2正規形違反にあたります。
商品名を変更するたびに、その商品が含まれる全注文のorder_itemsを更新しなければならず、更新漏れのリスクが生じます。
-- ✅ 第2正規形:商品情報をproductsテーブルへ分離
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL
);
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
商品名はproductsテーブルに一元化され、変更時の更新箇所が1箇所で済むようになりました。
第3正規形:推移的関数従属の排除
第3正規形は、主キーに直接従属しない列(主キー以外の列に従属する列)を排除するというルールです。
-- ❌ 正規化前:department_nameがdepartment_id経由で間接的に決まる
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
department_id INT NOT NULL,
department_name VARCHAR(100) NOT NULL -- department_id経由で決まる
);
department_nameは主キーのidに直接従属しているのではなく、department_idを経由して間接的に決まっています。
これを推移的関数従属と呼びます。
部署名が変わった場合、その部署に所属する全社員のレコードを更新する必要があり、更新漏れが起きれば同じ部署なのに社員ごとに部署名が食い違うという不整合が発生します。
-- ✅ 第3正規形:部署情報をdepartmentsテーブルへ分離
CREATE TABLE departments (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL
);
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
department_id INT NOT NULL,
FOREIGN KEY (department_id) REFERENCES departments(id)
);
正規化のメリット・デメリット比較
正規化はデータの整合性を高める一方で、テーブル数が増えJOINが多くなるというトレードオフを伴います。
| 観点 | 正規化する | 正規化を緩める(非正規化) |
|---|---|---|
| データ整合性 | 高い(1事実1箇所) | 更新漏れのリスクあり |
| 更新のしやすさ | 1箇所修正すれば反映される | 複数箇所を修正する必要がある場合も |
| 読み取り性能 | JOINが増え、複雑なクエリでは低下しやすい | JOINが減り、単純な読み取りは高速 |
| ストレージ | 重複が少なく効率的 | 重複データによる冗長化 |
| 向いている場面 | トランザクション処理(更新が多い) | 集計・レポート用途(読み取り中心) |
実務では、更新系の基幹テーブルは第3正規形まで正規化し、集計用の分析テーブルやキャッシュテーブルはあえて非正規化して結合コストを下げる、という使い分けが一般的です。
私が携わったECサイトの案件でも、注文一覧画面の表示速度がJOINの多さでボトルネックになった際、「集計済みの注文サマリテーブル」をあえて非正規化した形で用意し、バッチで定期更新することでパフォーマンスを改善した経験があります。
正規化は「常に第3正規形まで徹底すべきルール」ではなく、「整合性と性能のどちらを優先するかを判断するための道具」として捉えるのが実践的なスタンスです。
まとめ
この記事のポイント
- 正規化は「1つの事実は1箇所にだけ存在させる」ことを目指すプロセス
- 第1正規形は繰り返し項目の排除、第2正規形は複合主キーの部分従属の排除、第3正規形は推移的関数従属の排除
- 正規化を進めるほど整合性は高まるが、JOINが増え読み取り性能は低下しやすい
- 更新系テーブルは正規化を徹底し、集計・分析用途では非正規化を検討するのが実践的
次に読むべき記事
正規化でJOINが増えると気になってくるのが検索性能です。
次のカテゴリでは、検索を高速化する仕組みであるインデックスについて解説していきます。
→ 次の記事:インデックスとは?なぜ検索が速くなるのか
タグ: SQL, 上級者向け, 設計