データベース正規化の基本:第1・第2・第3正規形を実例で解説

black laptop computer turned-on displaying source code on table SQL

データベースを設計するとき、テーブル構造をどう整理するかで後々の保守性やクエリの書きやすさが大きく変わります。正規化(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

タイトルとURLをコピーしました