AI時代にあらためて学ぶ DB設計の正規化とアンチパターン
はじめに
AIでコードを書くのが当たり前になった今、アプリケーションのロジックは後から容易に書き直せるようになりました。もし実装を間違えたとしても、AIに頼めばすぐに修正案を出してくれます。
一方で、DBスキーマはそう簡単には変えられないです。
なぜなら、DBはビジネスロジックと強く結びついており、すでにユーザーが使っている状態でスキーマを変更すると、その影響は非常に大きくなります。
データの持ち方を誤ると、マイグレーションが難しくなったり、最悪の場合「データを移行できない」という状況に陥ることもあるかもしれません。
この問題は、AIがどれだけ進化しても変わることはないように思えます。
コードが簡単に書き直せるようになったからこそ、DB設計の重要性は相対的に高まっていると感じています。
だからこそ、アプリを作り始める段階でDB設計を軽視せず、将来ビジネスの形や要件が変わっても、できるだけ柔軟に対応できる設計をしておきたいところです。
そんな問題意識から、今回は DB設計における正規化の考え方と、アンチパターン について、改めて学び直してみました。
今回学び直すにあたって、『達人に学ぶDB設計徹底指南書 第2版』(ミック著)を読みました。
本書はDB設計の基本から実践的なポイントまで体系的にまとまっていて、正規化やアンチパターンを学ぶのにとても良い一冊です。
この記事では、本書の内容をベースに自分なりの理解を整理してみました。
正規化はなんのためにするのか
データベースを設計するうえで大切なのが「正規化」です。
正規化とは、データの持ち方から冗長性を排除し、一貫性と効率性を保つための手法です。
冗長性があると何が困るのか、具体的に見てみます。
たとえば、以下のような「注文テーブル」があったとします。
| 注文ID | 顧客名 | 顧客メールアドレス | 商品名 | 金額 |
|---|---|---|---|---|
| 1 | 田中太郎 | tanaka@example.com | 商品A | 1000 |
| 2 | 田中太郎 | tanaka@example.com | 商品B | 2000 |
| 3 | 鈴木花子 | suzuki@example.com | 商品A | 1000 |
田中太郎さんのメールアドレスが変わった場合、このテーブルでは注文ID 1と2の両方を更新しなければなりません。
もし片方だけ更新してしまうと、同じ人なのにメールアドレスが異なるという矛盾した状態になります。
レコードが2件なら目視で気づけますが、これが数万件、数十万件になったらどうでしょうか。
どこかで不整合が起きていても気づけない、という状況が容易に発生します。
こうした問題を防ぐために、正規化ではデータを適切にテーブル分割し、同じ情報を複数の場所に持たないようにします。
正規化のパターンは第1正規形から第5正規形まであります。段階が上がるほど冗長性がより排除された設計になります。
DB設計におけるトレードオフ
では、常に正規化を徹底すればいいのかというと、必ずしもそうとは限りません。
正規化するとデータの整合性は保ちやすくなりますが、テーブルが分割される分、データを取得する際にJOINが増え、パフォーマンスが低下することがあります。
先ほどの例で考えてみます。正規化すると、注文テーブルと顧客テーブルを分けることになります。
顧客テーブル
| 顧客ID | 顧客名 | 顧客メールアドレス |
|---|---|---|
| 1 | 田中太郎 | tanaka@example.com |
| 2 | 鈴木花子 | suzuki@example.com |
注文テーブル
| 注文ID | 顧客ID | 商品名 | 金額 |
|---|---|---|---|
| 1 | 1 | 商品A | 1000 |
| 2 | 1 | 商品B | 2000 |
| 3 | 2 | 商品A | 1000 |
データの整合性は保たれるようになりましたが、「注文一覧に顧客名も表示したい」という場合、2つのテーブルをJOINする必要があります。テーブルが2つならまだシンプルですが、正規化を進めるほどテーブル数は増えていき、JOINも複雑になっていきます。
つまり、正規化とパフォーマンスには強いトレードオフの関係があります。
- 正規化を徹底すれば → データの整合性は高まるが、検索パフォーマンスが落ちやすい
- パフォーマンスを優先すれば → データの不整合が発生しやすくなる
本書では、著者は原則として非正規化は許容しないとしつつも、パフォーマンスを改善する他の手段がすべて尽きた場合に限り、最後の手段として非正規化を認めるという立場をとっています。
基本的にはまず正規化を目指し、パフォーマンスの問題が出たときに初めて非正規化を検討する、という順序で考えるのが良さそうです。
次の章では、正規化の5つのパターンについて具体的に整理していきます。
正規化の5つのパターン
第1正規形(1NF) ― スカラ値の原則
第1正規形のルールはシンプルです。1つのセルには1つの値しか入れない。これを「スカラ値の原則」と呼びます。
まず、このルールに違反しているテーブルを見てみます。
非正規形のテーブル(社員と所属部署)
| 社員ID | 社員名 | 所属部署 |
|---|---|---|
| 1 | 田中太郎 | 営業部, 企画部 |
| 2 | 鈴木花子 | 開発部 |
| 3 | 佐藤次郎 | 営業部, 開発部, 総務部 |
田中太郎さんと佐藤次郎さんの「所属部署」に複数の値がカンマ区切りで入っています。これが第1正規形に違反している状態です。
では、これをどう直すか。方法は2つあります。
パターン①:列を増やす
| 社員ID | 社員名 | 所属部署1 | 所属部署2 | 所属部署3 |
|---|---|---|---|---|
| 1 | 田中太郎 | 営業部 | 企画部 | NULL |
| 2 | 鈴木花子 | 開発部 | NULL | NULL |
| 3 | 佐藤次郎 | 営業部 | 開発部 | 総務部 |
一見よさそうに見えますが、これには問題があります。所属部署が4つある人が出てきたら列を追加しなければなりませんし、所属が1つしかない人はNULLだらけになります。部署で検索したいときも「所属部署1〜3のどれかに含まれるか」を調べる必要があり、SQLも複雑になります。
パターン②:行を増やす
| 社員ID | 社員名 | 所属部署 |
|---|---|---|
| 1 | 田中太郎 | 営業部 |
| 1 | 田中太郎 | 企画部 |
| 2 | 鈴木花子 | 開発部 |
| 3 | 佐藤次郎 | 営業部 |
| 3 | 佐藤次郎 | 開発部 |
| 3 | 佐藤次郎 | 総務部 |
こちらの方がすっきりしています。1つのセルに1つの値だけが入っており、第1正規形を満たしています。部署が増えても行を追加するだけで済みますし、検索も簡単です。
ただし、これには1つ問題点があります。
それは、このテーブルの 主キーを決められない という問題です。
元のテーブルでは「社員ID」だけで各行を一意に特定できましたが、行を増やしたことで社員ID = 1の行が2つ存在します。そのため、主キーは「社員ID + 所属部署」の複合キーになります。
そして、主キーにはNULLを含めることができない というルールがあります。もし所属部署がNULLの社員がいたら、主キーとして機能しなくなってしまいます。
なぜ1つのセルに複数の値を入れてはいけないのか
なぜリレーショナルデータベースではこれが認められないのでしょうか。
その理由は 関数従属性 にあります。
関数従属性とは、「ある列の値が決まれば、別の列の値も一意に決まる」という関係のことです。
たとえば、社員テーブルで考えると以下のようになります。
社員ID → 社員名
社員ID = 1 と決まれば、社員名は「田中太郎」と一意に決まります。これが関数従属性です。
では、非正規形のテーブルに戻って考えてみましょう。
社員ID → 所属部署 ?
社員ID = 1 に対して「営業部, 企画部」というカンマ区切りの値が返ってきます。これは1つの値ではなく、実質的に2つの値です。主キーから各列の値を一意に決定できない、つまり関数従属性が成り立っていません。
リレーショナルデータベースは、この関数従属性を前提として設計されています。だからこそ、1つのセルには1つの値だけを入れる必要があるのです。
第2正規形(2NF) ― 部分関数従属の解消
第1正規形を満たしたテーブルをもう一度見てみます。
| 社員ID | 社員名 | 所属部署 |
|---|---|---|
| 1 | 田中太郎 | 営業部 |
| 1 | 田中太郎 | 企画部 |
| 2 | 鈴木花子 | 開発部 |
| 3 | 佐藤次郎 | 営業部 |
| 3 | 佐藤次郎 | 開発部 |
| 3 | 佐藤次郎 | 総務部 |
このテーブルは第1正規形を満たしていますが、第2正規形ではありません。
第2正規形で解消すべきなのは 部分関数従属 です。
部分関数従属とは
部分関数従属とは、主キーの一部の列に対して従属する列がある状態のことです。
つまり、主キー全体ではなく、その一部だけで値が決まってしまう列があるということです。
このテーブルの主キーは「社員ID + 所属部署」の複合キーでした。ここで、各列の関数従属性を確認してみます。
{社員ID, 所属部署} → 社員名 (主キー全体で決まる)
社員ID → 社員名 (主キーの一部だけでも決まる!)
「社員名」は主キー全体がなくても、社員IDだけで一意に決まります。これが部分関数従属です。
部分関数従属があると何が困るのか
① データを登録できないケースが生まれる
たとえば、新入社員の山田さんがまだどの部署にも配属されていない場合を考えます。
| 社員ID | 社員名 | 所属部署 |
|---|---|---|
| 4 | 山田三郎 | NULL |
主キーは「社員ID + 所属部署」なので、所属部署がNULLだと主キーが成立しません。つまり、部署が決まるまで山田さんを登録できないということになります。
② データの不整合が起きる可能性がある
社員名は社員IDだけで決まるはずなのに、複数行に同じ情報が重複しています。
| 社員ID | 社員名 | 所属部署 |
|---|---|---|
| 1 | 田中太郎 | 営業部 |
| 1 | 田中太朗 | 企画部 |
もし片方の行だけ社員名を間違えて更新してしまったら、同じ社員IDなのに名前が異なるという不整合が生まれます。
第2正規形にする
解消方法はシンプルです。部分関数従属している列を別テーブルに切り出します。
社員テーブル
| 社員ID | 社員名 |
|---|---|
| 1 | 田中太郎 |
| 2 | 鈴木花子 |
| 3 | 佐藤次郎 |
社員部署テーブル
| 社員ID | 所属部署 |
|---|---|
| 1 | 営業部 |
| 1 | 企画部 |
| 2 | 開発部 |
| 3 | 営業部 |
| 3 | 開発部 |
| 3 | 総務部 |
こうすることで、先ほどの2つの問題が解消されます。
- まだ配属先が決まっていない山田さんも、社員テーブルには登録できる
- 社員名は社員テーブルの1行だけで管理されるので、不整合が起きない
これが第2正規形です。ポイントは 「主キーの一部だけで決まる列を見つけて、別テーブルに分ける」 ということです。
第3正規形(3NF) ― 推移的関数従属の解消
第2正規形まで正規化した社員テーブルに、少し列を追加してみましょう。
社員テーブル(第2正規形)
| 社員ID | 社員名 | 部署ID | 部署名 |
|---|---|---|---|
| 1 | 田中太郎 | D01 | 営業部 |
| 2 | 鈴木花子 | D02 | 開発部 |
| 3 | 佐藤次郎 | D01 | 営業部 |
主キーは「社員ID」です。第2正規形は満たしていますが、第3正規形ではありません。
第3正規形で解消すべきなのは 推移的関数従属 です。
推移的関数従属とは
推移的関数従属とは、主キー以外の列を経由して、別の列の値が決まる 状態のことです。
このテーブルの関数従属性を確認してみます。
社員ID → 部署ID → 部署名
「部署名」は社員IDから直接決まるのではなく、部署IDを経由して決まっています。これが推移的関数従属です。テーブル内部に段階的な従属関係がある状態とも言えます。
推移的関数従属があると何が困るのか
① 誰も所属していない部署を登録できない
たとえば、新しく「広報部(D03)」を作ったけれど、まだ誰も配属されていない場合を考えます。
| 社員ID | 社員名 | 部署ID | 部署名 |
|---|---|---|---|
| ??? | ??? | D03 | 広報部 |
部署の情報を登録するには、社員IDが必要です。しかし、まだ誰もいないので登録できません。
② データの不整合が起きる可能性がある
部署ID「D01」に対応する部署名が複数行に重複しています。
| 社員ID | 社員名 | 部署ID | 部署名 |
|---|---|---|---|
| 1 | 田中太郎 | D01 | 営業部 |
| 3 | 佐藤次郎 | D01 | 営業企画部 |
片方の行だけ部署名を変更してしまうと、同じ部署IDなのに名前が異なるという不整合が生まれます。第2正規形で見た問題と同じパターンですね。
第3正規形にする
解消方法は第2正規形と同じく、テーブルを分割してそれぞれの関数従属の関係を独立させます。
社員テーブル
| 社員ID | 社員名 | 部署ID |
|---|---|---|
| 1 | 田中太郎 | D01 |
| 2 | 鈴木花子 | D02 |
| 3 | 佐藤次郎 | D01 |
部署テーブル
| 部署ID | 部署名 |
|---|---|
| D01 | 営業部 |
| D02 | 開発部 |
こうすることで、先ほどの問題が解消されます。
- まだ誰も所属していない「広報部」も、部署テーブルに登録できる
- 部署名は部署テーブルの1行だけで管理されるので、不整合が起きない
第3正規形のポイントは 「主キー以外の列への従属を見つけて、別テーブルに分ける」 ということです。
第2正規形では「主キーの一部」への従属を解消しましたが、第3正規形では「主キー以外の列」への従属を解消します。やっていることはどちらもテーブルの分割ですが、着目する従属関係が異なります。
ボイス・コッド正規形(BCNF) ― 非キーからキーへの関数従属の解消
第3正規形までは「主キー以外の列」への従属を解消してきました。ボイス・コッド正規形(BCNF)では、さらに踏み込んで 非キーからキーへの関数従属 も解消します。
3NFとBCNFの違いを一言で言うと、3NFは「主キー以外の列 → 主キー以外の列」の従属を排除しましたが、BCNFは「主キー以外の列 → 主キーの一部」の従属も排除します。
具体例で見てみます。
BCNFに違反しているテーブル
ある会社で、社員がチームに所属し、各チームにはチーム補佐がいるという状況を考えます。1つのチームに複数の補佐がいることはありますが、1人の補佐が複数のチームを兼任することはできないものとします。
チーム所属テーブル
| 社員ID | チームコード | チーム補佐 |
|---|---|---|
| 1 | T01 | 山田 |
| 1 | T02 | 佐藤 |
| 2 | T01 | 田中 |
| 2 | T03 | 鈴木 |
| 3 | T02 | 佐藤 |
主キーは「社員ID + チームコード」の複合キーです。
T01には山田と田中の2人の補佐がいますが、山田はT01だけ、田中もT01だけを担当しています。
このテーブルの関数従属性を整理してみます。
{社員ID, チームコード} → チーム補佐 (主キー全体で決まる)
チーム補佐 → チームコード (補佐が決まればチームも決まる ← これが問題!)
「チーム補佐 → チームコード」は、非キー(チーム補佐)から主キーの一部(チームコード)への関数従属です。3NFでは見逃されますが、BCNFではこれを解消する必要があります。
なお、「チームコード → チーム補佐」は成り立ちません。T01に対して山田と田中の2人がいるため、チームコードからチーム補佐を一意に決定できないからです。
この状態だと何が困るのか
① 複数行の更新が発生する
山田の担当チームがT01からT04に変わった場合、山田が含まれるすべての行を更新しなければなりません。更新漏れがあると不整合が生まれます。
② 関連を登録できない
新しく「高橋がチームT04の補佐になった」という情報を登録したくても、まだ誰もそのチームに所属していなければ社員IDがないため、登録できません。
③ レコードを削除すると関連情報も消える
社員ID = 2 がチームT03から外れた場合、そのレコードを削除すると「鈴木がT03の補佐である」という情報まで一緒に消えてしまいます。
BCNFにする ― 分解の注意点
「チーム補佐 → チームコード」の関数従属を解消するために、テーブルを分割します。
間違った分解:チームコードをキーにJOINする場合
まず、(社員ID, チームコード)と(チームコード, チーム補佐)に分解した場合を考えます。
チーム所属テーブル
| 社員ID | チームコード |
|---|---|
| 1 | T01 |
| 1 | T02 |
| 2 | T01 |
| 2 | T03 |
| 3 | T02 |
チーム補佐テーブル
| チーム補佐 | チームコード |
|---|---|
| 山田 | T01 |
| 田中 | T01 |
| 佐藤 | T02 |
| 鈴木 | T03 |
チーム補佐テーブルではT01に山田と田中の2人が紐づいています。チームコードをキーにJOINすると、T01に所属するすべての補佐が結びついてしまいます。
チームコードでJOINした結果
| 社員ID | チームコード | チーム補佐 | |
|---|---|---|---|
| 1 | T01 | 山田 | ○ 元のデータ通り |
| 1 | T01 | 田中 | ✖︎ 元のデータにない! |
| 1 | T02 | 佐藤 | ○ 元のデータ通り |
| 2 | T01 | 山田 | ✖︎ 元のデータにない! |
| 2 | T01 | 田中 | ○ 元のデータ通り |
| 2 | T03 | 鈴木 | ○ 元のデータ通り |
| 3 | T02 | 佐藤 | ○ 元のデータ通り |
元のテーブルは5行でしたが、JOIN結果は7行に増えてしまいました。
社員1はT01の補佐として山田だけが紐づくはずなのに、田中も紐づいています。社員2も同様に、T01の補佐は田中のはずなのに山田も紐づいてしまっています。
同じチームコードに複数の補佐がいるため、チームコードでJOINすると余分な組み合わせが生まれ、社員・チーム・チーム補佐の間に 現実には存在しない関連 が発生してしまうのです。
これが「JOINしても元のテーブルに戻らない」という問題です。このような分解は 損失分解 と呼ばれ、避けなければなりません。
正しい分解:チーム補佐をキーにJOINする場合
では、(社員ID, チーム補佐)と(チーム補佐, チームコード)に分解してみましょう。
チーム所属テーブル
| 社員ID | チーム補佐 |
|---|---|
| 1 | 山田 |
| 1 | 佐藤 |
| 2 | 田中 |
| 2 | 鈴木 |
| 3 | 佐藤 |
チーム補佐テーブル
| チーム補佐 | チームコード |
|---|---|
| 山田 | T01 |
| 田中 | T01 |
| 佐藤 | T02 |
| 鈴木 | T03 |
チーム補佐でJOINした結果
| 社員ID | チーム補佐 | チームコード | |
|---|---|---|---|
| 1 | 山田 | T01 | ○ |
| 1 | 佐藤 | T02 | ○ |
| 2 | 田中 | T01 | ○ |
| 2 | 鈴木 | T03 | ○ |
| 3 | 佐藤 | T02 | ○ |
5行のまま、元のデータと一致しています。1人の補佐は1つのチームしか担当しないので、チーム補佐をキーにJOINすれば余分な行は生まれません。これが 無損失分解 です。
BCNFへの分解では、関数従属性の方向を正しく理解し、適切なキーでJOINしないと、元のテーブルに戻せなくなるリスクがあることを理解しておくことが大切です。
第4正規形(4NF) ― 多値従属性の解消
ここまでの正規化では、主キーの一部や非キーへの関数従属を解消してきました。第4正規形では、多値従属性 という別の種類の問題を解消します。
具体例で見てみましょう。
4NFに違反しているテーブル
ある会社で、社員がチームに所属し、同時に製品も担当しているという状況を考えます。チームと製品の間には直接の関連はなく、それぞれ独立して社員に紐づいています。
社員チーム製品テーブル
| 社員ID | チームコード | 製品コード |
|---|---|---|
| 1 | T01 | P01 |
| 1 | T01 | P02 |
| 2 | T02 | P01 |
| 3 | T01 | P03 |
主キーは「社員ID + チームコード + 製品コード」の3列すべてで構成されています。
社員1はチーム「T01」に所属し、製品「P01, P02」を担当しています。チームは1つなのに、担当製品が2つあるので行が2つ必要になっています。
この状態だと何が困るのか
① チーム異動時に複数行の更新が必要になる
社員1がチームT01からT02に異動した場合、複数行に対する更新をしなければなりません。
| 社員ID | チームコード | 製品コード |
|---|---|---|
| 1 | T01 → T02 | P01 |
| 1 | T01 → T02 | P02 |
② NULLを指定できない
まだ製品を担当していない社員4をチームT03に登録したい場合を考えます。
| 社員ID | チームコード | 製品コード |
|---|---|---|
| 4 | T03 | NULL |
主キーは3列すべてなので、製品コードがNULLだと主キーが成立しません。製品を担当していない社員のチーム所属を登録できないのです。
多値従属性とは
このテーブルの問題は、社員IDに対して チームの集合 と 製品の集合 がそれぞれ独立して紐づいていることにあります。
社員ID →→ チームコード (社員が決まればチームの集合が決まる)
社員ID →→ 製品コード (社員が決まれば製品の集合が決まる)
この →→ の関係を 多値従属性 と呼びます。通常の関数従属性(→)が「1つの値が決まる」のに対して、多値従属性は「値の集合が決まる」という関係です。
チームと製品の間には何の関連もないのに、同じテーブルに入っているせいで、すべての組み合わせを持たなければならなくなっています。
4NFにする
解消方法はシンプルです。独立した関連をそれぞれ別のテーブルに分けます。
社員チームテーブル
| 社員ID | チームコード |
|---|---|
| 1 | T01 |
| 2 | T02 |
| 3 | T01 |
社員製品テーブル
| 社員ID | 製品コード |
|---|---|
| 1 | P01 |
| 1 | P02 |
| 2 | P01 |
| 3 | P03 |
こうすることで、先ほどの問題が解消されます。
- 社員1がT01からT02に異動しても、社員チームテーブルの1行を更新するだけで済む
- 製品を担当していない社員のチーム所属も、社員チームテーブルには問題なく登録できる
- チームと製品の無関係な組み合わせを持つ必要がなくなる
第4正規形のポイントは、1つのテーブルには1つの関連だけを持たせる ということです。互いに独立した関連を同じテーブルに詰め込むと、不要な組み合わせが生まれてデータの管理が複雑になります。関連ごとにテーブルを分けることで、それぞれを独立して管理できるようになります。
第5正規形(5NF) ― 結合従属性の解消
第4正規形では、互いに独立した多値従属性を別テーブルに分けました。しかし、分けた関連同士が実は独立ではなく、業務上のルールで結びついている 場合はどうでしょうか。
第5正規形では、この問題を解消します。
4NFの例を振り返る
先ほどの第4正規形では、チームと製品は無関係という前提でテーブルを分けました。
社員チームテーブル
| 社員ID | チームコード |
|---|---|
| 1 | T01 |
| 2 | T02 |
| 3 | T01 |
社員製品テーブル
| 社員ID | 製品コード |
|---|---|
| 1 | P01 |
| 1 | P02 |
| 2 | P01 |
| 3 | P03 |
しかし、ここで業務上のルールが加わったとします。「チームによって扱う製品が異なる」 というルールです。
たとえば、T01はP01とP03を扱い、T02はP01とP02を扱う、というようにチームと製品の間にも関連があるケースです。
5NFに違反している状態
この場合、社員・チーム・製品の3者の間にはそれぞれ関連があります。
社員ID →→ チームコード (社員がどのチームに所属しているか)
社員ID →→ 製品コード (社員がどの製品を担当しているか)
チームコード →→ 製品コード (チームがどの製品を扱っているか)
4NFの時点では、社員とチーム、社員と製品の2つのテーブルに分解しました。しかし、チームと製品の間にも関連があるのに、それを表すテーブルがありません。
この状態だと何が困るのでしょうか。
たとえば、社員1はチームT01に所属し、製品P01とP02を担当しています。しかし、T01が扱う製品はP01とP03だけで、P02は扱いません。
社員製品テーブル
| 社員ID | 製品コード | |
|---|---|---|
| 1 | P01 | ○ T01で扱っている |
| 1 | P02 | ✖︎ T01では扱っていない! |
社員とチーム、社員と製品の2つのテーブルだけでは、「社員1がT01に所属しているのにT01で扱っていないP02を担当している」という矛盾をテーブル構造で防ぐことができません。
5NFにする
解消方法は、3者の間にある関連をすべて独立したテーブルとして表現する ことです。
社員チームテーブル(社員がどのチームに所属しているか)
| 社員ID | チームコード |
|---|---|
| 1 | T01 |
| 2 | T02 |
| 3 | T01 |
チーム製品テーブル(チームがどの製品を扱っているか)
| チームコード | 製品コード |
|---|---|
| T01 | P01 |
| T01 | P03 |
| T02 | P01 |
| T02 | P02 |
社員製品テーブル(社員がどの製品を担当しているか)
| 社員ID | 製品コード |
|---|---|
| 1 | P01 |
| 1 | P03 |
| 2 | P01 |
| 3 | P03 |
3つのテーブルに分けたことで、それぞれの関連が独立して管理されるようになりました。
- 社員1はT01に所属している(社員チームテーブル)
- T01が扱う製品はP01とP03である(チーム製品テーブル)
- 社員1が担当する製品はP01とP03である(社員製品テーブル)
すべてが整合しています。先ほどのように「T01で扱っていないP02を社員1が担当している」という矛盾は、チーム製品テーブルを参照することで検知できるようになります。
第5正規形のポイント
第4正規形では「独立した関連は別テーブルに分ける」でしたが、第5正規形のポイントは 「関連がある場合は、それに対応する関連エンティティ(テーブル)を作る」 ということです。
2者間の関連だけでなく、3者以上の間に存在するすべての関連をテーブルとして表現することで、データの整合性をより厳密に保つことができます。
正規化のまとめ
ここまで第1正規形から第5正規形まで見てきました。最後に、正規化のポイントを整理しておきます。
正規化の3つのポイント
① 正規化とは、更新時の不都合・不整合を排除するために行う
正規化の目的は、データの冗長性をなくし、更新・削除・挿入時に矛盾が生じない構造にすることです。各正規形で見てきたように、正規化されていないテーブルでは「複数行の更新が必要」「登録できないデータがある」「削除すると関連情報まで消える」といった問題が発生します。
② 正規化は、従属性を見抜くことで可能になる
正規化の作業は、テーブルの中にある従属性を見つけ出し、それを適切に分解することです。
-
関数従属性(
→):主キーの一部や非キーへの従属(第2正規形〜BCNF) -
多値従属性(
→→):キーに対して値の集合が従属する関係(第4正規形) - 結合従属性:3者以上の間に存在する関連(第5正規形)
どの従属性がテーブルに潜んでいるかを見抜くことが、正規化の本質です。
③ 正規化は、いつでも非正規形に戻せる
正規化によってテーブルを分割しても、JOINすれば元のテーブルを復元できます。これは正規化が無損失分解であるからです。つまり、正規化は不可逆な操作ではありません。必要に応じて非正規形に戻す(=非正規化する)選択肢も常に残されています。
正規化はどこまですべきか
第3正規形までは原則として行う のがよいとされています。第3正規形まで正規化すれば、実務で問題になる冗長性の大半は解消されます。
BCNF以降は、テーブル構造だけでは守れない制約が出てくるなどのトレードオフもあるため、ビジネスルールやパフォーマンス要件を踏まえて判断することになります。
また、関連エンティティが存在する場合は、関連とエンティティが1対1に対応するようにテーブルを設計する ことが大切です。第4正規形・第5正規形で見たように、独立した関連を1つのテーブルに詰め込むと不要な組み合わせが生まれ、関連があるのにテーブルがないと整合性を保てなくなります。
まとめ
今回は『達人に学ぶDB設計徹底指南書 第2版』をもとに、正規化の考え方を第1正規形から第5正規形まで整理してみました。
正規化の本質は、テーブルに潜む従属性を見抜き、データの冗長性を排除することです。それによって「複数行の更新が必要」「登録できないデータがある」「削除すると関連情報まで消える」といった更新時の不整合を防ぐことができます。
実務では第3正規形まで押さえておけば多くのケースに対応できますが、BCNF以降の考え方を知っておくことで、より複雑なビジネスルールに対しても適切な設計判断ができるようになると感じました。
冒頭でも触れた通り、AIによってコードは簡単に書き直せる時代になりましたが、DBスキーマはそう簡単には変更できません。開発の初期段階で正規化の原則に沿った設計をしておくことが、将来の変更に強いアプリケーションを作る土台になると思います。
本書にはこの記事では触れなかったアンチパターンについても詳しく解説されているので、気になった方はぜひ手に取ってみてください。
Discussion