0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【DB用語の歯がゆさ 第5回】正規化 ―「第3正規形まで」の「まで」って何?

0
Posted at

株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。

Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
コーポレートサイト

このシリーズについて

「知ってはいる。けど、人に説明しろと言われると詰まる」——そんな歯がゆいDB用語を、1記事1用語・図解中心で解消していくシリーズです。

まず自己診断

「テーブル設計は第3正規形までやっておけばいい」——現場でよく聞くフレーズです。では:

その「まで」は何ですか。第4正規形があるのに、なぜ3で止めるんですか。

ここで詰まる人が多いはずです。第1〜第3正規形の手順を暗記していても、「なぜ3が実務の着地点なのか」は教わらないからです。

答えを先に言うと、3NFまでで「よくある事故」がほぼ全部消えるからです。それを実際に見ていきます。

正規化の目的はひとつだけ

正規化の目的は、テーブルを増やすことでも、暗記した手順を踏むことでもありません。

同じ事実を、1箇所にしか書かない。

これだけです。同じ事実が複数箇所にあると、片方だけ更新されて矛盾します。

実測:正規化していないと何が起きるか

口で言うより速いので、動かします。sqlite3(macOS標準) で、受講記録を1枚の表に詰め込んだ「悪い設計」を作ります。

CREATE TABLE enrollments_bad (
  student_id   INTEGER NOT NULL,
  student_name TEXT    NOT NULL,
  course_id    INTEGER NOT NULL,
  course_name  TEXT    NOT NULL,
  teacher_name TEXT    NOT NULL,
  teacher_tel  TEXT    NOT NULL,
  PRIMARY KEY (student_id, course_id)
);
INSERT INTO enrollments_bad VALUES
 (1,'佐藤',101,'SQL入門','山田','090-1111'),
 (2,'鈴木',101,'SQL入門','山田','090-1111'),
 (3,'高橋',101,'SQL入門','山田','090-1111'),
 (1,'佐藤',102,'Java入門','田中','090-2222'),
 (2,'鈴木',104,'SQL応用','山田','090-1111');

山田先生(SQL入門とSQL応用の2講座を担当)の電話番号が、4行に重複しています。ここから3つの事故が起きます。

① 更新不整合:片方だけ変わる

山田先生の番号が変わったので更新します。ただし、うっかり条件を絞ってしまいました。

UPDATE enrollments_bad SET teacher_tel = '090-9999'
 WHERE teacher_name = '山田' AND student_id = 1;

SELECT student_name, course_name, teacher_name, teacher_tel FROM enrollments_bad;
student_name  course_name  teacher_name  teacher_tel
------------  -----------  ------------  -----------
佐藤          SQL入門      山田          090-9999
鈴木          SQL入門      山田          090-1111
高橋          SQL入門      山田          090-1111
佐藤          Java入門     田中          090-2222
鈴木          SQL応用      山田          090-1111

同じ山田先生に2つの電話番号が生まれました。確認します。

SELECT teacher_name, COUNT(DISTINCT teacher_tel) AS tel_kinds
  FROM enrollments_bad GROUP BY teacher_name;
teacher_name  tel_kinds
------------  ---------
山田          2
田中          1

どちらが正しいのか、DBには判断できません。

② 挿入不整合:受講者がいない講座を登録できない

新しい講座を作りたいのですが、まだ受講者がいません。

INSERT INTO enrollments_bad (course_id, course_name, teacher_name, teacher_tel)
VALUES (103,'Python入門','伊藤','090-3333');
Error: stepping, NOT NULL constraint failed: enrollments_bad.student_id (19)

講座を登録するのに、受講者をでっち上げないといけないという異常な状態です。

③ 削除不整合:消したくないものまで消える

Java入門の受講登録を取り消します。

DELETE FROM enrollments_bad WHERE course_id = 102;
SELECT COUNT(*) FROM enrollments_bad WHERE teacher_name = '田中';
0

田中先生の連絡先が、この世から消えました。 消したかったのは受講記録だけなのに。

1NF → 2NF → 3NF:1枚の表が割れていく

この3つの事故を消す作業が正規化です。段階ごとに「何が消えるか」を対にして見ます。

第1正規形(1NF):繰り返しをなくす

1行に「SQL入門, Java入門」のように複数の値を詰め込まない、という段階です。

悪い例(非1NF) student_id courses
1 SQL入門, Java入門

これを1講座1行に展開します。表がSQLで扱える形になるのがゴールで、ここはまだ事故は消えません。

第2正規形(2NF):キーの一部にしか依存しない列を追い出す

主キーは (student_id, course_id) の複合キーです。ここで各列を見ると、キー全体を見なくても決まる列があります。

student_namestudent_id だけで決まります。キーの一部にしか依存していないこの状態を部分関数従属と呼び、これを分離するのが2NFです。

ここで②挿入不整合と③削除不整合が消えます。 講座の情報が courses 表に独立するので、受講者ゼロでも講座を登録でき、受講記録を消しても講座は残ります。

第3正規形(3NF):非キー列に依存する列を追い出す

2NF後の courses 表にはまだ問題があります。

teacher_telcourse_id で直接決まるのではなく、teacher_name を経由して決まっています。これが推移的関数従属teachers 表として切り出すのが3NFです。

これは「理屈のための一手間」ではありません。2NFの courses 表を実際に見ると、山田先生が2講座を担当しているせいでまだ重複が残っています

course_id  name      teacher_name  teacher_tel
---------  --------  ------------  -----------
101        SQL入門   山田          090-1111
102        Java入門  田中          090-2222
104        SQL応用   山田          090-1111

この状態で片方の講座だけ更新すると、やはり矛盾します。

UPDATE courses SET teacher_tel = '090-9999' WHERE course_id = 101;
SELECT teacher_name, COUNT(DISTINCT teacher_tel) AS tel_kinds FROM courses GROUP BY teacher_name;
teacher_name  tel_kinds
------------  ---------
山田          2
田中          1

2NFでは①更新不整合が消えきらない。 だから3NFが要るわけです。

段階 やること 消える事故
1NF 繰り返しをなくす (まだ消えない)
2NF 部分関数従属を除去 ②挿入不整合・③削除不整合
3NF 推移的関数従属を除去 ①更新不整合

3NFを終えた時点で、冒頭の3つが全部消えている——これが「まで」の正体です。

実測:正規化後は事故が起きない

同じ操作を3NFの構成でやり直します。

-- 2講座を持つ山田先生も、更新は1行だけ
UPDATE teachers SET tel = '090-9999' WHERE name = '山田';

SELECT c.name AS course, t.name AS teacher, t.tel
  FROM courses c JOIN teachers t ON c.teacher_id = t.teacher_id ORDER BY c.course_id;
course    teacher  tel
--------  -------  --------
SQL入門   山田     090-9999
Java入門  田中     090-2222
SQL応用   山田     090-9999

2つの講座の両方に、正しい番号が反映されました。矛盾の起こしようがありません。電話番号を書いた行が、そもそも1行しかないからです。

-- 受講者ゼロの講座も登録できる
INSERT INTO courses VALUES (103,'Python入門',3);
SELECT course_id, name FROM courses;
course_id  name
---------  ----------
101        SQL入門
102        Java入門
103        Python入門
-- 受講記録を消しても講師は残る
DELETE FROM enrollments WHERE course_id = 102;
SELECT COUNT(*) FROM teachers WHERE name = '田中';
1

3つとも解消しました。

「まで」の答えと、その先

BCNF以降が存在しないわけではありません。並べるとこうです。

正規形 除去する対象 実務での登場頻度
BCNF 候補キーが複数あり重なる場合の従属 まれ
4NF 多値従属 かなりまれ
5NF 結合従属 ほぼ理論の世界

3NFで止めるのは、手抜きではなく費用対効果です。BCNF以降が対象とするのは「候補キーが複数あって重なり合う」ような特殊な構造で、普通の業務テーブルではめったに現れません。一方でテーブルを割るほどJOINは増えます。得られる安全性と払うコストが釣り合う地点が3NF、というのが「まで」の意味です。

なお3NFとBCNFは、候補キーが1つだけのテーブルでは一致します。多くの実務テーブルは3NFにした時点で自動的にBCNFも満たしています

非正規化は「手抜き」ではなく「取引」

最後に実務の注意です。あえて正規化を崩す非正規化という選択があります。

重要なのは、非正規化はチューニングの一種ではなく、トレードオフの選択だという点です。「速いから非正規化」ではなく、「更新不整合のリスクを引き受けてでも読み取り速度が要る」と判断したときだけ選びます。しかも計測して初めて判断できることなので、最初から非正規化で設計するのは、ただの設計ミスです。

第1回で「DBが守るのは宣言した制約だけ」と書きました。非正規化は、その守りを自分から手放す行為でもあります。

まとめ:1行で説明するなら

「正規化の目的は同じ事実を1箇所にしか書かないことです。1NFで繰り返しをなくし、2NFで部分関数従属を除いて挿入・削除の不整合を、3NFで推移的関数従属を除いて更新不整合を消します。よくある事故が3NFで出尽くすので、そこが実務の着地点。BCNF以降が扱うのは候補キーが重なる特殊なケースで、めったに出番がありません」

「第3正規形まで」の「まで」は、**「事故が消えきる地点まで」**という意味です。

次回

第6回:インデックス ——「貼ったのに効かないのはなぜ?」を図解します。

(シリーズ全11回の予定は第1回に掲載しています)

参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?