さかした
達人に学ぶDB設計から学ぶ「正規化」の本当の目的
2026年05月30日
見出しはありません
要約を生成中...
アプリ開発をしていると、
テーブルをどう分けるべきか
同じ情報を複数箇所に持ってよいのか
Prismaのschemaはどこまで分割するべきか
と悩むことがあります。
私自身、Next.jsとPrismaを使った個人開発を進める中で、DB設計について改めて学び直したくなり『達人に学ぶDB設計 徹底指南書』を読みました。
その中でも特に印象に残ったのが「正規化」です。
正規化というと、
「テーブルを細かく分割する作業」
というイメージを持たれがちですが、本書を読んで認識が大きく変わりました。
この記事では、『達人に学ぶDB設計 徹底指南書』から学んだ正規化の考え方を、Next.js + Prisma の実例を交えながらまとめてみます。
正規化とは何か
正規化とは、
データの重複をなくし、整合性を保ちやすくするための設計手法
です。
例えば、次のようなPrismaスキーマがあったとします。
model User {
id String @id
name String
groupName String
}
データは以下のようになります。
id | name | groupName |
|---|---|---|
1 | 田中 | 開発部 |
2 | 鈴木 | 開発部 |
3 | 佐藤 | 営業部 |
一見問題なさそうです。
しかし、
「開発部」を「システム開発部」に変更したい場合、
複数レコードを更新する必要があります。
更新漏れが発生すると、
id | name | groupName |
|---|---|---|
1 | 田中 | システム開発部 |
2 | 鈴木 | 開発部 |
という不整合が発生します。
なぜ正規化が必要なのか
本書で最も印象に残ったのは、
正規化の目的は
データの整合性を守ること
であるという点です。
多くの人が、
正規化 = テーブル分割
と考えます。
しかし本質は違います。
重要なのは、
一つの事実を一箇所だけで管理する
ことです。
第1正規形
第1正規形では、
繰り返し項目を排除する
ことを目指します。
例えば、
model User {
id String @id
name String
hobby1 String?
hobby2 String?
hobby3 String?
}
この設計では趣味が増えるたびにカラムを追加しなければなりません。
そこで、
model User {
id String @id
name String
hobbies Hobby[]
}
model Hobby {
id String @id
name String
userId String
user User @relation(fields: [userId], references: [id])
}
とします。
これによって趣味が何個増えても対応できます。
第2正規形
第2正規形では、
主キーの一部だけに依存するデータ
を分離します。
例えば、
model OrderItem {
orderId String
productId String
productName String
@@id([orderId, productId])
}
ここで、
productName
は
productId
だけで決まります。
つまり複合主キー全体には依存していません。
そのため、
model Product {
id String @id
name String
}
model OrderItem {
orderId String
productId String
product Product @relation(
fields: [productId],
references: [id]
)
@@id([orderId, productId])
}
へ分離します。
第3正規形
第3正規形では、
主キー以外の項目への依存
を排除します。
例えば、
model User {
id String @id
zipCode String
address String
}
住所が郵便番号から決まる場合、
id
↓
zipCode
↓
address
という依存関係になります。
このような状態を解消するのが第3正規形です。
実務で重要なのは「データの持ち主」
本書を読んで感じたのは、
正規化はルールを暗記することではない
ということです。
重要なのは、
そのデータは誰が管理すべきか
を考えることです。
例えばOgoRouletteで考えてみます。
悪い例です。
model Room {
id String @id
ownerId String
ownerName String
}
ユーザー名を変更すると、
全てのRoomを更新しなければなりません。
良い例です。
model User {
id String @id
name String
rooms Room[]
}
model Room {
id String @id
ownerId String
owner User @relation(
fields: [ownerId],
references: [id]
)
}
ユーザー名はUserだけが管理します。
RoomはUserへの参照だけを持ちます。
正規化しすぎても良くない
実務では、
正規化 = 正義
ではありません。
例えば、
User
Group
Expense
Payment
AuditLog
Notification
などを細かく分けた結果、
複雑なJOINが増えることがあります。
Prismaでは、
const group = await prisma.group.findUnique({
where: {
id: groupId,
},
include: {
members: {
include: {
user: true,
},
},
expenses: true,
audits: true,
},
})
のような取得になります。
便利ですが、
大量データになると危険です。
正規化とOOMの関係
最近、OOMについて学ぶ機会がありました。
OOM(Out Of Memory)は、
大量データを一度にメモリへ載せることで発生します。
例えば、
const groups = await prisma.group.findMany({
include: {
members: {
include: {
user: true,
},
},
},
})
で、
グループ 10,000件
メンバー 200,000件
ユーザー 200,000件
を取得した場合、
メモリ使用量が急増します。
結果として、
メモリ増加
↓
GC頻発
↓
レスポンス低下
↓
OOM
につながる可能性があります。
正規化とパフォーマンスは別問題
ここで重要なのは、
正規化
≠
パフォーマンス改善
ということです。
正規化は整合性のために行います。
パフォーマンス改善は、
ページネーション
インデックス
キャッシュ
クエリ最適化
などで行います。
目的を混同しないことが重要です。
個人開発で活かしたい考え方
私が今後の開発で意識したいのは、
「このデータの持ち主は誰か?」
を考えることです。
例えば、
Soraであれば、
ユーザー
ペット
思い出
写真
それぞれ責任範囲を明確にします。
Lernovaであれば、
ユーザー
学習記録
カテゴリ
バッジ
の責務を分離します。
こうすることで、
後から機能追加しやすい設計になります。
まとめ
『達人に学ぶDB設計 徹底指南書』を読んで感じたのは、
正規化とは単なるテーブル分割の技術ではなく、
データの事実を一箇所で管理するための考え方
だということです。
そして、
正規化は整合性
パフォーマンスは別問題
という考え方も非常に重要でした。
今後もDB設計を行う際は、
このデータの持ち主は誰か
重複していないか
本当にここに置くべきか
を意識しながら設計していきたいと思います。
コメント
まだコメントはありません。