なぜ正規化が必要か

このレッスンで分かること

  • 非正規化テーブルは更新異常・挿入異常・削除異常を引き起こします
  • 正規化のゴールは「1 つの事実が 1 か所だけ」に存在する状態です
  • テーブルを責務で分けて外部キーでつなぐのが基本方針

なぜ正規化が必要か とは

データベース設計における正規化の重要性と、その必要性について解説します。本レッスンでは、なぜ正規化が必要か の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

導入

業務システムを長く運用していると、テーブル設計の良し悪しが運用コストとして跳ね返ってきます。同じ顧客名が何十行にも繰り返し書かれ、住所変更のたびに全行を更新しなければならない。表計算ソフトをそのままテーブルにしたような設計では、データの 更新異常 / 挿入異常 / 削除異常 が発生し、整合性が崩れていきます。正規化 はこうした異常を理論的に防ぐための手続きで、リレーショナルデータベース設計の出発点です。

非正規化テーブルで起きる異常

たとえば、注文と顧客と商品を 1 枚のテーブルにまとめた次のような構造を考えてみましょう。

SQL クエリ

CREATE TABLE orders_flat ( order_id INT, customer_id INT, customer_name VARCHAR(100), customer_email VARCHAR(255), product_id INT, product_name VARCHAR(100), unit_price INT, quantity INT );

この 1 枚テーブルでは、同じ顧客の情報が注文行ごとに重複します。顧客のメールアドレスが変更された場合、関連する全行を漏れなく書き換える必要があり、1 行でも更新漏れがあれば矛盾が発生します。これが 更新異常 (update anomaly) です。

SQL クエリ

-- 更新漏れの例: 顧客 1 のメールを変えるつもりが、注文行 3 を更新し忘れた UPDATE orders_flat SET customer_email = 'new@example.com' WHERE customer_id = 1 AND order_id <> 3;

さらに、まだ注文していない顧客を登録できない 挿入異常 (insertion anomaly)、最後の注文を消すと顧客情報まで消える 削除異常 (deletion anomaly) も起きます。

異常が起きる流れ

diagram (will load when visible)

図の流れを箇条書きで整理すると、次の通りです。

  • 1 枚テーブルに全てを格納する
  • 同じ値が複数行で重複する
  • 更新時に書き漏らしが発生する (更新異常)
  • 親に紐付かないと子を登録できない (挿入異常)
  • 子を消すと親も道連れに消える (削除異常)

正規化のゴール

正規化のゴールは、1 つの事実が 1 か所だけに存在する状態を作ることです。住所は customers に、商品名は products に、注文の数量は order_items にといったように、責務でテーブルを分け、外部キー でつなぎます。

SQL クエリ

CREATE TABLE customers ( id INT PRIMARY KEY, name VARCHAR(100), email VARCHAR(255) ); CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), unit_price INT ); CREATE TABLE order_items ( order_id INT, product_id INT, quantity INT, PRIMARY KEY (order_id, product_id) );

代表例

非正規化テーブルから「顧客ごとの注文金額合計」を出すと、商品単価が重複しているため集計値がブレるリスクがあります。

SQL クエリ

-- 非正規化テーブルでは unit_price が手入力で変わっていることがある SELECT customer_id, SUM(unit_price * quantity) AS total FROM orders_flat GROUP BY customer_id;

正規化されたテーブルなら、商品マスタが 1 か所のため、価格の整合性は壊れません。

SQL クエリ

SELECT o.customer_id, SUM(p.unit_price * oi.quantity) AS total FROM order_items oi JOIN products p ON p.id = oi.product_id JOIN orders o ON o.id = oi.order_id GROUP BY o.customer_id;

正規化は「データ重複をゼロにする」のが本当の目的ではありません。重複を許すと事実の単一性が壊れ、整合性が保てなくなることが本質的な問題です。

最初から「全部 1 枚のテーブル」にする設計は、Excel をそのまま DB に持ち込んだような状態です。短期的には楽でも、データが増えるほど更新コストが指数関数的に膨らみます。

更新異常・挿入異常・削除異常という呼び方はリレーショナルデータベースの世界で 1970 年代から使われている用語で、現代の業務システム設計でも変わらず通用します。

3 つの異常を具体例で理解する

更新異常

同じ顧客のメールアドレスが 5 つの注文行に書かれているとします。アドレス変更時にどれか 1 行でも更新を漏らすと、過去の注文を辿るたびに「メールアドレスはどれが正しいのか」が分からなくなります。アプリ側で SELECT DISTINCT email FROM orders_flat WHERE customer_id = 1 を実行すると複数行が返ってきて、ユーザに送るアドレスを 1 つに絞れなくなります。

挿入異常

注文 1 つにつき 1 行という制約があるので、注文がない新規顧客は登録できません。仮に NULL で埋めて空の注文行を作れば、今度は集計対象としてカウントしてしまい、別の不具合になります。

削除異常

顧客 A の最後の注文を取り消すと、その顧客の名前とメールアドレスもテーブルから消滅します。注文と顧客は本来別の事実なのに、1 枚テーブルにまとめたせいで命運を共にしてしまいます。

正規化と SQL 設計

正規化はテーブル分割の話ですが、SQL 自体の書き方にも影響します。正規化されたスキーマでは集計のために JOIN が必要になりますが、その分マスタは 1 か所なのでクエリの意味が安定します。一方、非正規化テーブルでは SELECT は簡単に書けますが、結果がいつも「最新の事実」であるとは限らなくなります。

この章のポイント

ここまでの要点 正規化 = 1 つの事実を 1 か所に。テーブルを責務で分けて FK でつなぐ。非正規化は更新・挿入・削除の 3 つの異常を生む。

まとめ

  • 非正規化テーブルは更新異常 / 挿入異常 / 削除異常を起こす
  • 正規化は「1 つの事実が 1 か所だけ」に存在する状態を目指す
  • テーブルを責務で分けて外部キーで結ぶのが基本方針
  • 集計は JOIN で組み立てれば不整合は起きない

次のレッスン

次は 第1正規形 (1NF) です。データベース設計の基礎、第一正規形(1NF)について、SQLでの具体例を通して実践的に理解を深めます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. 正規化の必要性 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 正規化の必要性 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

テーブル構造

schema.sql

CREATE TABLE orders_flat ( order_id INT, customer_id INT, customer_name VARCHAR(100), customer_email VARCHAR(255), product_id INT, product_name VARCHAR(100), unit_price INT, quantity INT ); INSERT INTO orders_flat VALUES (1001, 1, '田中', 'tanaka@example.com', 10, 'ノート', 300, 2), (1001, 1, '田中', 'tanaka@example.com', 11, 'ペン', 150, 4), (1002, 2, '佐藤', 'sato@example.com', 10, 'ノート', 300, 1), (1003, 1, '田中', 'tanaka_NEW@example.com', 12, '消しゴム', 100, 3), (1004, 3, '鈴木', 'suzuki@example.com', 11, 'ペン', 150, 5);

期待される出力

customer_iddistinct_email_count
12

ヒント

query.sql
query.sql
学習モード
コードの実行結果
データベースを初期化中...