データベースを設計するとき、テーブル構造をどう整理するかで後々の保守性やクエリの書きやすさが大きく変わります。正規化(Normalization)はその整理手順を体系化したもので、第1・第2・第3正規形の3段階でテーブルを整えていきます。この記事では受注管理システムを例に、非正規形から第3正規形まで変換する手順を順を追って解説します。各ステップで「何が問題で、なぜ分割するのか」という理由も一緒に示します。
このシナリオで考える
架空のECサイトが受注データを1テーブルに詰め込んでいる状況をスタート地点にします。注文・顧客・商品の情報をすべて1行に収めようとした、よくある非正規形の例です。
CREATE TABLE orders_bad (
order_id INT,
order_date DATE,
customer_id INT,
customer_name VARCHAR(100),
customer_zip CHAR(8),
prefecture VARCHAR(20), -- 郵便番号から導出できる冗長な値
items TEXT -- 例: '商品A×2個, 商品B×1個'(複数値)
);
サンプルデータのイメージは次のとおりです。
| order_id | customer_name | customer_zip | prefecture | items |
|---|---|---|---|---|
| 1001 | 田中 太郎 | 150-0001 | 東京都 | 商品A×2個, 商品B×1個 |
| 1002 | 鈴木 花子 | 530-0001 | 大阪府 | 商品A×3個 |
items 列に複数の商品が1セルに詰め込まれており、prefecture は郵便番号があれば導出できる冗長な値です。これを3段階で整えていきます。
第1正規形(1NF):繰り返しを排除する
第1正規形の要件は「1つのセルに値が1つだけ入ること」です。items 列のように複数の値を1セルに詰め込んでいると、「商品Aだけを集計する」「数量を合計する」といった操作がSQLで書けません。解消方法は「1行=1商品」になるよう行を分けることです。
-- 1NF後:1行1商品に分解
CREATE TABLE orders_1nf (
order_id INT,
order_date DATE,
customer_id INT,
customer_name VARCHAR(100),
customer_zip CHAR(8),
prefecture VARCHAR(20),
product_id INT,
product_name VARCHAR(100),
unit_price DECIMAL(10,2),
quantity INT,
PRIMARY KEY (order_id, product_id) -- 複合主キー
);
| order_id | customer_id | customer_name | product_id | product_name | quantity |
|---|---|---|---|---|---|
| 1001 | 101 | 田中 太郎 | 201 | 商品A | 2 |
| 1001 | 101 | 田中 太郎 | 202 | 商品B | 1 |
| 1002 | 102 | 鈴木 花子 | 201 | 商品A | 3 |
主キーが (order_id, product_id) の複合キーになり、SQLで自由に集計・検索できる形になりました。ただし、customer_name が複数行に重複しています。田中 太郎の名前を変更するなら2行を更新しなければなりません。次のステップで解消します。
第2正規形(2NF):部分従属を解消する
第2正規形の要件は「すべての非キー属性が主キー全体に依存していること」です。複合主キーの一部にしか依存していない属性を「部分従属(Partial Dependency)」と呼び、これを分離します。
今のテーブルで依存関係を整理するとこうなります。
| 属性 | 依存する主キー | 判定 |
|---|---|---|
| quantity | (order_id, product_id) 全体 | ✓ 完全従属 |
| order_date | order_id のみ | ✗ 部分従属 |
| customer_name | customer_id のみ(order_id の一部) | ✗ 部分従属 |
| product_name / unit_price | product_id のみ | ✗ 部分従属 |
部分従属している属性を役割ごとのテーブルに切り出します。
-- 2NF後:テーブルを役割ごとに分割
CREATE TABLE orders (
order_id INT PRIMARY KEY,
order_date DATE,
customer_id INT
);
CREATE TABLE order_details (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id)
);
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(100),
customer_zip CHAR(8),
prefecture VARCHAR(20) -- まだ残っている
);
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100),
unit_price DECIMAL(10,2)
);
これで customers テーブルは顧客ごとに1行になり、名前の重複がなくなりました。ただし customers テーブルをよく見ると、prefecture(都道府県)は customer_zip(郵便番号)が決まれば自動的に決まります。この間接的な依存が3NF違反です。
第3正規形(3NF):推移従属を解消する
第3正規形の要件は「非キー属性が他の非キー属性に依存していないこと」です。このような間接的な依存を「推移的関数従属(Transitive Dependency)」と呼びます。
customers テーブルでは customer_id → customer_zip → prefecture という依存の連鎖があります。prefecture は主キーに直接依存しているのではなく、customer_zip を経由して間接的に決まっています。
| customer_id | customer_name | customer_zip | prefecture |
|---|---|---|---|
| 101 | 田中 太郎 | 150-0001 | 東京都 |
| 102 | 鈴木 花子 | 530-0001 | 大阪府 |
| 103 | 佐藤 次郎 | 150-0002 | 東京都 |
郵便番号が「150-0001」と「150-0002」は別のコードですが、どちらも東京都です。同じ都道府県名が複数行に格納されており、変更が発生した場合は複数行を更新しなければなりません。customer_zip → prefecture の依存を別テーブルに分離します。
-- 3NF後:推移従属を解消
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(100),
customer_zip CHAR(8)
);
CREATE TABLE zip_codes (
zip CHAR(8) PRIMARY KEY,
prefecture VARCHAR(20)
);
これで customers テーブルの属性はすべて主キー(customer_id)に直接従属するようになりました。都道府県名のメンテナンスは zip_codes テーブルの1行を更新するだけです。最終的なテーブル構成は5テーブルになります。
| テーブル | 役割 | 主キー |
|---|---|---|
| orders | 注文ヘッダ | order_id |
| order_details | 注文明細(1行1商品) | (order_id, product_id) |
| customers | 顧客マスタ | customer_id |
| products | 商品マスタ | product_id |
| zip_codes | 郵便番号マスタ | zip |
よくある落とし穴 / 注意点
正規化を進めるときに初心者がつまずきやすいポイントをまとめます。
| 落とし穴 | 詳細 |
|---|---|
| 2NFは複合主キーのテーブルだけが対象 | 単一列の主キーには「一部に依存」という概念がない。部分従属は複合主キーを持つテーブルでのみ発生する |
| 関数従属の判断はデータではなく業務ルール | テーブルのデータを眺めるだけでは判断できないケースがある。「この値はあの値が決まれば一意に定まるか」を業務要件から確認する |
| NULL の増加に注意 | テーブルを分割すると JOIN が増え、LEFT JOIN 後にNULLが混入しやすくなる。COUNT や SUM の集計時はNULL扱いを意識する |
| 正規化しすぎも問題 | テーブルを極端に細分化するとJOINが増えてクエリが複雑になる。実務では性能や開発効率を考慮し、意図的に非正規化(denormalization)することもある |
まとめ
正規化は「繰り返しグループの排除(1NF)→ 部分従属の解消(2NF)→ 推移従属の解消(3NF)」という3段階で進めます。各ステップで「なぜ分割するのか」を意識すると理解が深まります。第3正規形まで整えることが一般的な目安ですが、最終的には業務要件と性能のバランスで設計の落としどころを判断してください。
参考リンク
アイキャッチ画像: Photo by Jantine Doornbos on Unsplash
