■ この記事はこんな人におすすめ
- SQLを書き始めて、JOINを多用するようになってきた人
- 「正規化って本当に正しいの?」と疑問を感じたことがある人
- パフォーマンスチューニングに興味がある初・中級エンジニア
- 非正規化という言葉は聞いたことあるけど、具体的にどんなものかわからない人
■ この記事で得られること
- 正規化がパフォーマンスに与える影響が理解できます
- 非正規化のパターンと、それぞれのトレードオフが判断できるようになります
- 正規化と非正規化を「使い分ける」判断軸が持てるようになります
■ 参考書籍
本記事は上記書籍の5章の内容をもとに構成しています。
1. 結論
正規化は原則として正しい選択です。ただし、パフォーマンス上の限界に達したとき、非正規化は有効な「最後の手段」になります。
正規化されたテーブルは、データの整合性を保ちやすく、更新の手間も少ない。しかしJOINが増えるほど、クエリのコストは上がっていきます。非正規化はその問題を解決できる一方で、「更新コストの増大」と「データのリアルタイム性の低下」という2つのリスクを抱えます。
この記事では、具体的なSQL例を通じてその仕組みを理解していきましょう!
2. まずは下記を理解しよう!
■ ① 正規化がパフォーマンスに影響するケースを把握する
JOINの回数・種類によって、パフォーマンスへの影響度が変わります。
以下のテーブル構成を例に考えます:
会社テーブル
| 会社コード | 会社名 |
|---|---|
| C0001 | A 商事 |
| C0002 | B 化学 |
| C0003 | C 建設 |
社員テーブル
| 会社コード | 社員ID | 社員名 | 年齢 | 部署コード |
|---|---|---|---|---|
| C0001 | E0001 | 加藤 | 40 | D01 |
| C0001 | E0002 | 藤本 | 32 | D02 |
| C0002 | E0002 | 田島 | 25 | D01 |
部署テーブル
| 部署コード | 部署名 |
|---|---|
| D01 | 開発 |
| D02 | 人事 |
| D03 | 営業 |
■ ② パターン別にJOINのコストを比較する 🔍
【パターン1】テーブル2つを内部結合
-- 会社名と社員名を取得
SELECT 会社.会社名, 社員.社員名
FROM 社員
INNER JOIN 会社 ON 会社.会社コード = 社員.会社コード
WHERE 社員.社員名 = '田島';
【パターン2】テーブル3つを内部結合
-- 会社名・社員名・部署名を取得
SELECT 会社.会社名, 社員.社員名, 部署.部署名
FROM 社員
INNER JOIN 会社 ON 会社.会社コード = 社員.会社コード
INNER JOIN 部署 ON 社員.部署コード = 部署.部署コード
WHERE 社員.社員名 = '田島';
【パターン3】外部結合で集計
-- 会社ごとの社員数を集計(社員がいない会社もNULLで表示)
SELECT 会社.会社コード, COUNT(社員.社員名) AS 社員数
FROM 会社
LEFT OUTER JOIN 社員 ON 社員.会社コード = 会社.会社コード
GROUP BY 会社.会社コード;
✅ パターン1〜3は、適切なインデックスが設定されていれば、パフォーマンス差はほぼ誤差レベルです。
■ ③ 非正規化で結合をゼロにするパターンを知る
【パターン4】非正規化テーブル(JOINなし)
会社名・部署名を社員テーブルに持たせてしまう設計です。
| 会社コード | 会社名 | 社員ID | 社員名 | 年齢 | 部署コード | 部署名 |
|---|---|---|---|---|---|---|
| C0001 | A 商事 | E0001 | 加藤 | 40 | D01 | 開発 |
| C0001 | A 商事 | E0002 | 藤本 | 32 | D02 | 人事 |
| C0002 | B 化学 | E0002 | 田島 | 25 | D01 | 開発 |
-- JOINなしで取得できる ✨
SELECT 会社名, 社員名, 部署名
FROM 社員
WHERE 社員名 = '田島';
このパターンは明らかに速いです。ただし、後述するリスクとのトレードオフがあります⚠️
■ ④ 非正規化の2パターン(実務でよく使う)を理解する
受注テーブルと受注明細テーブルを使った例で考えます。
受注テーブル
| 受注ID | 受注日 | 注文者名義 |
|---|---|---|
| 0001 | 2026-01-05 | 岡野 徹 |
| 0002 | 2026-01-06 | 浜田 健一 |
| 0003 | 2026-01-08 | 石井 慶子 |
受注明細テーブル
| 受注ID | 受注明細連番 | 商品名 |
|---|---|---|
| 0001 | 1 | マカロン |
| 0001 | 2 | 紅茶 |
| 0002 | 1 | 紅茶 |
| 0002 | 2 | 日本茶 |
📅 非正規化パターンA:明細テーブルに「受注日」を持たせる
受注日での範囲絞り込みを高速化したいとき、受注明細に受注日カラムを追加します。
| 受注ID | 受注明細連番 | 商品名 | 受注日 |
|---|---|---|---|
| 0001 | 1 | マカロン | 2026-01-05 |
| 0001 | 2 | 紅茶 | 2026-01-05 |
| 0002 | 1 | 紅茶 | 2026-01-06 |
➡️ JOINなしで WHERE 受注日 BETWEEN '...' AND '...' が実行できます。
🔢 非正規化パターンB:受注テーブルに「商品数(集計値)」を持たせる
受注日別の集計を高速化したいとき、受注テーブルに商品数カラムを追加します。
| 受注ID | 受注日 | 注文者名義 | 商品数 |
|---|---|---|---|
| 0001 | 2026-01-05 | 岡野 徹 | 4 |
| 0002 | 2026-01-06 | 浜田 健一 | 3 |
| 0003 | 2026-01-08 | 石井 慶子 | 1 |
➡️ GROUP BY不要で集計結果が即座に取れます。
■ ⑤ 非正規化の「2つのリスク」を認識する
非正規化には必ず以下のコストが伴います。導入前に必ず評価してください。
🔴 リスク1:更新パフォーマンスの低下
- 会社名が変わった場合 → 全社員レコードの「会社名」カラムを一括更新する必要がある
- 商品が追加・削除された場合 → 受注テーブルの「商品数」も更新が必要になる
🔴 リスク2:データのリアルタイム性の低下
- 集計値(商品数など)を都度更新すると、DBへの書き込み負荷が増える
- 更新頻度を下げると、集計値が実態とずれる(リアルタイム性が失われる)
3. 各項目の説明
■ なぜパターン1〜3のパフォーマンス差は誤差レベルなのか?
JOINは「インデックスさえ適切に設定されていれば」、DBエンジンが効率的に処理します。会社コードや部署コードに対してインデックスが貼られていれば、結合対象の絞り込みは非常に高速に動作します。
2〜3テーブルのJOINが問題になるのは、インデックス設計が不十分な場合や、結合結果が非常に大きくなる場合(例:数百万件同士のJOIN)です。
■ パターン4(非正規化)はなぜ速いのか?
結合処理そのものが不要になるからです。1テーブルへのシンプルなSELECTは、複数テーブルを組み合わせる処理と比べてオーバーヘッドが少なく、クエリプランも単純になります。特に読み取り頻度が非常に高く、更新が少ないケースで効果を発揮します。
■ 非正規化を採用すべき判断基準
以下の条件が揃ったとき、非正規化を検討する価値があります:
- ✅ 正規化+インデックス最適化を行っても、パフォーマンスが要件を満たせない
- ✅ 対象のデータが読み取り頻度が高く、更新頻度が低い
- ✅ データの不整合リスクをアプリケーション側でコントロールできる
逆に、更新が頻繁なデータに非正規化を適用すると、更新コストと不整合リスクが膨らみ、正規化以上のパフォーマンス問題を引き起こすことがあります🚨
まとめ
- 正規化によるJOINのパフォーマンス低下は、インデックスが適切なら誤差レベル
- 非正規化(JOINなし)は明確に速いが、トレードオフがある
- 非正規化の2つのリスク:①更新コストの増大 と ②リアルタイム性の低下
- 非正規化の代表パターン:関連カラムをテーブルに持たせる / 集計値をテーブルに持たせる
- 原則は正規化。非正規化はパフォーマンスチューニングの最後の手段 🏁
結局何をすればいいの❓
- 🏗️ まず正規化を徹底する(第3正規形を目指す)
- 🔑 インデックスを適切に設計する(結合キー・検索条件カラムを中心に)
- 📊 パフォーマンス計測を行う(EXPLAINでクエリプランを確認)
- 🔧 それでも不足なら非正規化を検討(影響範囲・更新頻度を必ず評価する)
- 🔒 非正規化導入後は更新処理の整合性担保を忘れずに
株式会社シンシア
株式会社xincereでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら
シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。