Lab AI データベース正規化の基礎——正規形の役割、データ異常の防止、実践的な設計判断
目次

リレーショナルデータベースの設計において、正規化(Normalization) はデータの一貫性と整合性を保つための基本技術だ。適切に正規化されたデータベースは、データの重複を排除し、異常な挙動(データ異常)を防止する。しかし、過度な正規化はパフォーマンスを劣化させることもある。

本記事では、データベース正規化の基本概念から、各正規形(1NF, 2NF, 3NF, BCNF)の定義と役割、データ異常の具体例、そして実践的な設計判断における正規化と非正規化のトレードオフまでを体系的に解説する。

正規化の基本概念——なぜ「正規化」が必要なのか

データ異常(Data Anomalies)の問題

正規化の主な目的は、データ異常を防止することだ。データ異常には 3 種類ある。

【データ異常の 3 種類】

1. **更新異常(Update Anomaly)**
   - 同じデータを複数の場所で更新する必要がある
   - 更新漏れでデータ不整合が発生

   例:社員マスターで「所属部署」を重複保持
   社員 A の部署変更 → 5 箇所のレコード更新が必要
   1 箇所でも漏れたら? → データ不整合

2. **挿入異常(Insertion Anomaly)**
   - 既存データがないと新規データを挿入できない
   - 不要な NULL 値が発生

   例:「注文」テーブルに「顧客情報」も含める
   新規顧客の登録 → 注文がないと顧客データを保存できない

3. **削除異常(Deletion Anomaly)**
   - データを削除したら、他のデータも失われる
   - 意図しない情報損失

   例:「注文」テーブルに「商品情報」も含める
   最後の注文を削除 → 商品情報も消滅

正規化の本質: 「データの依存関係を整理し、各事実を 1 箇所だけで保持する」

正規化のプロセス

【正規化のステップ】

非正規形(UNF)
    ↓ 重複グループを分離
第 1 正規形(1NF)
    ↓ 部分関数従属を排除
第 2 正規形(2NF)
    ↓ 推移的関数従属を排除
第 3 正規形(3NF)
    ↓ 決定キーが候補キーのみ
BC 正規形(BCNF)
    ↓ 多値従属を排除
第 4 正規形(4NF)
    ↓ 結合従属を排除
第 5 正規形(5NF)

実務的な目安: 通常は 3NF または BCNF までで十分

第 1 正規形(1NF)—— 繰り返しグループの排除

1NF の定義

第 1 正規形(First Normal Form, 1NF) は、すべての属性(カラム)が原子値(これ以上分割できない値) を持つ状態だ。

【非正規形の例】

注文テーブル(非正規形):
┌──────────┬─────────────────────────────────────┐
│ 注文 ID  │ 商品                                │
├──────────┼─────────────────────────────────────┤
│ 1001     │ スマホ,ケース,充電器              │
│ 1002     │ ノート PC                           │
│ 1003     │ タブレット,キーボード,マウス      │
└──────────┴─────────────────────────────────────┘

問題点:
・「商品」カラムが複数の値を持つ(カンマ区切り)
・「3 番目の商品は何か?」というクエリが書けない
・商品の追加・削除が困難
【1NF 適用後】

注文明細テーブル(1NF):
┌──────────┬──────────────┬─────────┐
│ 注文 ID  │ 商品 ID       │ 数量    │
├──────────┼──────────────┼─────────┤
│ 1001     │ P001         │ 1       │
│ 1001     │ P002         │ 1       │
│ 1001     │ P003         │ 1       │
│ 1002     │ P010         │ 1       │
│ 1003     │ P020         │ 1       │
│ 1003     │ P021         │ 1       │
│ 1003     │ P022         │ 1       │
└──────────┴──────────────┴─────────┘

改善点:
・各カラムが単一の値
・「商品 ID ごとの集計」が容易
・商品の追加・削除が簡単

1NF への変換コード

def convert_to_1nf(non_1nf_data):
    """
    非正規形データを 1NF に変換

    Parameters:
        non_1nf_data: 繰り返しグループを持つデータ

    Returns:
        list: 1NF 適用後のデータ(各行が原子値)
    """
    nf1_data = []

    for row in non_1nf_data:
        order_id = row['order_id']
        customer = row['customer']
        # 繰り返しグループを展开
        for product in row['products']:  # 商品のリスト
            nf1_row = {
                'order_id': order_id,
                'customer': customer,
                'product_id': product['id'],
                'product_name': product['name'],
                'quantity': product['quantity'],
                'unit_price': product['unit_price']
            }
            nf1_data.append(nf1_row)

    return nf1_data

# 使用例
non_1nf = [
    {
        'order_id': 1001,
        'customer': '山田',
        'products': [
            {'id': 'P001', 'name': 'スマホ', 'quantity': 1, 'unit_price': 80000},
            {'id': 'P002', 'name': 'ケース', 'quantity': 1, 'unit_price': 3000},
            {'id': 'P003', 'name': '充電器', 'quantity': 1, 'unit_price': 5000},
        ]
    },
    {
        'order_id': 1002,
        'customer': '鈴木',
        'products': [
            {'id': 'P010', 'name': 'ノート PC', 'quantity': 1, 'unit_price': 120000},
        ]
    }
]

nf1_result = convert_to_1nf(non_1nf)

print("【1NF 変換結果】")
for row in nf1_result:
    print(f"注文{row['order_id']}: {row['product_name']} × {row['quantity']}")

出力:

【1NF 変換結果】
注文 1001: スマホ × 1
注文 1001: ケース × 1
注文 1001: 充電器 × 1
注文 1002: ノート PC × 1

1NF の重要点: 「各セルに 1 つの値」——これがリレーショナルデータベースの前提条件

第 2 正規形(2NF)—— 部分関数従属の排除

関数従属(Functional Dependency)の概念

関数従属は、ある属性の値が決まれば、別の属性の値が一意に決まる関係だ。

【関数従属の例】

社員テーブル:
┌─────────┬──────────┬────────────┬──────────────┐
│ 社員 ID │ 社員名   │ 所属部署   │ 部署所在地   │
├─────────┼──────────┼────────────┼──────────────┤
│ E001    │ 山田     │ 営業部     │ 東京         │
│ E002    │ 鈴木     │ 営業部     │ 東京         │
│ E003    │ 田中     │ 開発部     │ 大阪         │
└─────────┴──────────┴────────────┴──────────────┘

関数従属関係:
- 社員 ID → 社員名(社員 ID が決まれば社員名が一意に決まる)
- 社員 ID → 所属部署
- 所属部署 → 部署所在地(部署が決まれば所在地が一意に決まる)
- 社員 ID → 部署所在地(推移的に決まる)

表記: 社員 ID → 社員名

部分関数従属とは

部分関数従属(Partial Functional Dependency) は、複合キーの一部だけで他の属性が決まってしまう状態だ。

【部分関数従属の例】

受注明細テーブル(1NF 済み):
┌──────────┬──────────┬────────────┬──────────────┬────────┐
│ 注文 ID  │ 商品 ID  │ 商品名     │ 商品価格     │ 数量   │
├──────────┼──────────┼────────────┼──────────────┼────────┤
│ 1001     │ P001     │ スマホ     │ 80,000       │ 1      │
│ 1001     │ P002     │ ケース     │ 3,000        │ 1      │
│ 1002     │ P001     │ スマホ     │ 80,000       │ 2      │
└──────────┴──────────┴────────────┴──────────────┴────────┘

主キー: {注文 ID, 商品 ID}(複合キー)

関数従属分析:
- {注文 ID, 商品 ID} → 数量 ✓(両方必要)
- 商品 ID → 商品名 ✗(商品 ID だけで決まる = 部分関数従属)
- 商品 ID → 商品価格 ✗(商品 ID だけで決まる = 部分関数従属)

問題:商品情報が注文 ID に関係なく商品 ID だけで決まる
→ 商品の情報が重複して保存される(データ冗長)

2NF の定義と変換

第 2 正規形(Second Normal Form, 2NF) は、1NF を満たし、かつすべての非キー属性が主キーに完全関数従属する状態だ。

【2NF への変換】

元のテーブル(1NF):
受注明細 (注文 ID, 商品 ID, 商品名,商品価格,数量)

変換後(2NF):
1. 受注明細 (注文 ID, 商品 ID, 数量)
   - 主キー:{注文 ID, 商品 ID}
   - 数量は両方に依存

2. 商品 (商品 ID, 商品名,商品価格)
   - 主キー:商品 ID
   - 商品名・価格は商品 ID のみに依存

結果:
- 商品情報の重複が消除
- 商品価格の変更 → 商品テーブルのみ更新

2NF 変換の実装

def convert_to_2nf(order_details):
    """
    部分関数従属を排除して 2NF に変換

    Parameters:
        order_details: 1NF の受注明細データ

    Returns:
        tuple: (受注明細,商品マスタ)
    """
    # 商品マスタの抽出(商品 ID をキーに一意化)
    products = {}
    for row in order_details:
        product_id = row['product_id']
        if product_id not in products:
            products[product_id] = {
                'product_id': product_id,
                'product_name': row['product_name'],
                'unit_price': row['unit_price']
            }

    # 受注明細から商品情報を分離
    order_lines = []
    for row in order_details:
        order_line = {
            'order_id': row['order_id'],
            'product_id': row['product_id'],
            'quantity': row['quantity']
        }
        order_lines.append(order_line)

    return order_lines, list(products.values())

# 使用例
order_data = [
    {'order_id': 1001, 'product_id': 'P001', 'product_name': 'スマホ', 'unit_price': 80000, 'quantity': 1},
    {'order_id': 1001, 'product_id': 'P002', 'product_name': 'ケース', 'unit_price': 3000, 'quantity': 1},
    {'order_id': 1002, 'product_id': 'P001', 'product_name': 'スマホ', 'unit_price': 80000, 'quantity': 2},
]

order_lines, products = convert_to_2nf(order_data)

print("【2NF 変換結果】")
print("\n受注明細:")
for line in order_lines:
    print(f"  注文{line['order_id']}: 商品{line['product_id']} × {line['quantity']}")

print("\n商品マスタ:")
for prod in products:
    print(f"  {prod['product_id']}: {prod['product_name']} ({prod['unit_price']:,}円)")

出力:

【2NF 変換結果】

受注明細:
  注文 1001: 商品 P001 × 1
  注文 1001: 商品 P002 × 1
  注文 1002: 商品 P001 × 2

商品マスタ:
  P001: スマホ (80,000 円)
  P002: ケース (3,000 円)

2NF の効果: 商品情報の重複がなくなり、価格変更時の更新漏れを防止

第 3 正規形(3NF)—— 推移的関数従属の排除

推移的関数従属(Transitive Dependency)

推移的関数従属は、A → B かつ B → C のとき、A → C が成り立つ関係だ。

【推移的関数従属の例】

社員テーブル(2NF 済み):
┌─────────┬──────────┬────────────┬──────────────┬──────────┐
│ 社員 ID │ 社員名   │ 所属部署   │ 部署所在地   │ 社員住所 │
├─────────┼──────────┼────────────┼──────────────┼──────────┤
│ E001    │ 山田     │ 営業部     │ 東京         │ 埼玉     │
│ E002    │ 鈴木     │ 営業部     │ 東京         │ 千葉     │
│ E003    │ 田中     │ 開発部     │ 大阪         │ 京都     │
└─────────┴──────────┴────────────┴──────────────┴──────────┘

関数従属:
- 社員 ID → 社員名 ✓
- 社員 ID → 所属部署 ✓
- 所属部署 → 部署所在地 ✓(← ここが問題)
- 社員 ID → 部署所在地(推移的に決まる)

推移的関数従属: 社員 ID → 所属部署 → 部署所在地

問題:部署所在地は「所属部署」を介して間接的に決まる
→ 部署所在地の情報が社員数分重複

3NF の定義と変換

第 3 正規形(Third Normal Form, 3NF) は、2NF を満たし、かつすべての非キー属性が他の非キー属性に非推移的である状態だ。

【3NF への変換】

元のテーブル(2NF):
社員 (社員 ID, 社員名,所属部署,部署所在地,社員住所)

変換後(3NF):
1. 社員 (社員 ID, 社員名,所属部署,社員住所)
   - 主キー:社員 ID

2. 部署 (所属部署,部署所在地)
   - 主キー:所属部署

結果:
- 部署所在地の重複が消除(営業部=東京が 1 行のみ)
- 部署の所在地変更 → 部署テーブルのみ更新
- 社員の異動 → 社員テーブルのみ更新

3NF 変換の実装

def convert_to_3nf(employees_2nf):
    """
    推移的関数従属を排除して 3NF に変換

    Parameters:
        employees_2nf: 2NF の社員データ

    Returns:
        tuple: (社員,部署)
    """
    # 部署マスタの抽出
    departments = {}
    for emp in employees_2nf:
        dept_name = emp['department']
        if dept_name not in departments:
            departments[dept_name] = {
                'department_name': dept_name,
                'location': emp['department_location']
            }

    # 社員データから部署所在地を分離
    employees_3nf = []
    for emp in employees_2nf:
        emp_3nf = {
            'employee_id': emp['employee_id'],
            'employee_name': emp['employee_name'],
            'department': emp['department'],
            'employee_address': emp['employee_address']
            # department_location は削除
        }
        employees_3nf.append(emp_3nf)

    return employees_3nf, list(departments.values())

# 使用例
employees_2nf = [
    {'employee_id': 'E001', 'employee_name': '山田', 'department': '営業部', 'department_location': '東京', 'employee_address': '埼玉'},
    {'employee_id': 'E002', 'employee_name': '鈴木', 'department': '営業部', 'department_location': '東京', 'employee_address': '千葉'},
    {'employee_id': 'E003', 'employee_name': '田中', 'department': '開発部', 'department_location': '大阪', 'employee_address': '京都'},
    {'employee_id': 'E004', 'employee_name': '佐藤', 'department': '開発部', 'department_location': '大阪', 'employee_address': '兵庫'},
]

employees_3nf, departments = convert_to_3nf(employees_2nf)

print("【3NF 変換結果】")
print("\n社員マスタ:")
for emp in employees_3nf:
    print(f"  {emp['employee_id']}: {emp['employee_name']} ({emp['department']})")

print("\n部署マスタ:")
for dept in departments:
    print(f"  {dept['department_name']}: {dept['location']}")

出力:

【3NF 変換結果】

社員マスタ:
  E001: 山田(営業部)
  E002: 鈴木(営業部)
  E003: 田中(開発部)
  E004: 佐藤(開発部)

部署マスタ:
  営業部:東京
  開発部:大阪

3NF の効果:

  • 部署所在地の重複が 4 行→2 行に削減
  • 部署移転時の更新コストが最小限に
  • データ不整合のリスクが低減

BC 正規形(BCNF)—— 決定キーの厳密化

3NF の例外ケース

3NF であっても、複数の候補キーがある場合に問題が発生することがある。

【3NF だが BCNF でない例】

講義登録テーブル:
┌──────────┬─────────┬────────────┐
│ 学生 ID  │ 科目 ID │ 担当教員   │
├──────────┼─────────┼────────────┤
│ S001     │ CS101   │ 山田       │
│ S001     │ CS102   │ 鈴木       │
│ S002     │ CS101   │ 山田       │
│ S002     │ CS103   │ 田中       │
└──────────┴─────────┴────────────┘

前提条件:
1. 1 学生は複数科目を履修できる
2. 1 科目は複数の学生が履修できる
3. **1 科目の担当教員は 1 人のみ**
4. **1 教員は 1 科目のみ担当**

関数従属:
- {学生 ID, 科目 ID} → 担当教員 ✓(主キーからの依存)
- 科目 ID → 担当教員 ✓(← 問題:科目 ID は非キー属性)
- 担当教員 → 科目 ID ✓(← 問題:教員も科目を一意に決定)

候補キー:
- {学生 ID, 科目 ID}
- {学生 ID, 担当教員}(教員→科目なので)

問題:
- 科目 ID → 担当教員 の関数従属がある
- 科目 ID は候補キーではない
- 3NF では「非キー属性が他の非キー属性に依存」をチェックするが
  この場合、科目 ID は非キー属性ではない(主キーの一部)

BCNF の定義

ボイス・コッド正規形(Boyce-Codd Normal Form, BCNF) は、3NF を強化した形で、すべての決定キーが候補キーである状態だ。

【BCNF の定義】

リレーション R において、すべての非自明な関数従属 X → Y に対し、
X が**超キー(スーパーキー)**である場合、R は BCNF を満たす。

言い換え:
- 「何かを一意に決定する属性」は必ず「主キーまたはその一部」でなければならない

BCNF への変換

【BCNF への変換】

元のテーブル(3NF):
講義登録 (学生 ID, 科目 ID, 担当教員)

変換後(BCNF):
1. 履修登録 (学生 ID, 科目 ID)
   - 主キー:{学生 ID, 科目 ID}
   - 学生と科目の多対多関係を表現

2. 科目担当 (科目 ID, 担当教員)
   - 主キー:科目 ID
   - または 担当教員(1 対 1 のため両方が候補キー)

結果:
- 関数従属「科目 ID → 担当教員」が別テーブルに分離
- 教員の変更 → 科目担当テーブルのみ更新
- 学生の履修変更 → 履修登録テーブルのみ更新

BCNF 変換の実装

def convert_to_bcnf(course_enrollments):
    """
    BCNF への変換

    Parameters:
        course_enrollments: 3NF の講義登録データ

    Returns:
        tuple: (履修登録,科目担当)
    """
    # 科目担当マスタの抽出(科目 ID → 担当教員)
    course_instructors = {}
    for row in course_enrollments:
        course_id = row['course_id']
        if course_id not in course_instructors:
            course_instructors[course_id] = row['instructor']

    # 履修登録データ(学生 ID, 科目 ID のみ)
    enrollments = []
    for row in course_enrollments:
        enrollment = {
            'student_id': row['student_id'],
            'course_id': row['course_id']
        }
        enrollments.append(enrollment)

    # 科目担当データ
    course_teachers = [
        {'course_id': cid, 'instructor': inst}
        for cid, inst in course_instructors.items()
    ]

    return enrollments, course_teachers

# 使用例
enrollments_3nf = [
    {'student_id': 'S001', 'course_id': 'CS101', 'instructor': '山田'},
    {'student_id': 'S001', 'course_id': 'CS102', 'instructor': '鈴木'},
    {'student_id': 'S002', 'course_id': 'CS101', 'instructor': '山田'},
    {'student_id': 'S002', 'course_id': 'CS103', 'instructor': '田中'},
]

enrollments_bcnf, course_teachers = convert_to_bcnf(enrollments_3nf)

print("【BCNF 変換結果】")
print("\n履修登録:")
for enr in enrollments_bcnf:
    print(f"  学生{enr['student_id']}: 科目{enr['course_id']}")

print("\n科目担当:")
for ct in course_teachers:
    print(f"  科目{ct['course_id']}: {ct['instructor']}教員")

出力:

【BCNF 変換結果】

履修登録:
  学生 S001: 科目 CS101
  学生 S001: 科目 CS102
  学生 S002: 科目 CS101
  学生 S002: 科目 CS103

科目担当:
  科目 CS101: 山田教員
  科目 CS102: 鈴木教員
  科目 CS103: 田中教員

正規化の実践的判断——いつ正規化し、いつ非正規化するか

正規化のメリット・デメリット

【正規化のメリット】

1. **データ一貫性の確保**
   - 単一事実の原則(1 つの事実を 1 箇所のみで保持)
   - 更新時の不整合リスク低減

2. **ストレージ効率**
   - データ重複の排除
   - 大規模システムで効果大

3. **保守性**
   - スキーマ変更に強い
   - 影響範囲の局所化

4. **挿入・削除異常の防止**
   - 独立したデータ追加が可能
   - 不要なデータ損失を防止
【正規化のデメリット】

1. **クエリのパフォーマンス低下**
   - JOIN 操作が増加
   - 複雑なクエリが多発

2. **クエリの複雑化**
   - 開発者の SQL スキルが要求
   - デバッグが困難

3. **インデックス設計の難易度上昇**
   - 複合インデックスの設計が複雑
   - クエリ実行計画の最適化が困難

非正規化(Denormalization)の判断基準

【非正規化が有効なケース】

1. **読み取り重視のワークロード**
   - 例:レポーティング、分析クエリ
   - 例:Web サイトの商品一覧表示

2. **頻繁な JOIN がボトルネック**
   - 例:毎回 5 つ以上のテーブルを結合
   - 例:ページ表示に 10 以上の JOIN が必要

3. **集計クエリの最適化**
   - 例:売上合計を毎回 SUM() で計算
   - 解決:集計値を事前計算して保持

4. **頻繁に参照するマスタデータ**
   - 例:商品名、部署名などの表示
   - 解決:冗長化してもパフォーマンス優先

非正規化の実装パターン

def denormalize_for_read_performance(orders, order_lines, products, customers):
    """
    読み取りパフォーマンスのための非正規化

    Parameters:
        orders: 注文テーブル(正規化済み)
        order_lines: 注文明細テーブル(正規化済み)
        products: 商品マスタ(正規化済み)
        customers: 顧客マスタ(正規化済み)

    Returns:
        list: 非正規化された注文ビュー(表示用)
    """
    denormalized_view = []

    for order in orders:
        customer = next(c for c in customers if c['customer_id'] == order['customer_id'])

        order_summary = {
            'order_id': order['order_id'],
            'order_date': order['order_date'],
            'customer_name': customer['customer_name'],  # 冗長化
            'customer_prefecture': customer['prefecture'],  # 冗長化
            'items': []
        }

        # 注文明細の商品情報を結合
        lines = [l for l in order_lines if l['order_id'] == order['order_id']]
        for line in lines:
            product = next(p for p in products if p['product_id'] == line['product_id'])
            order_summary['items'].append({
                'product_name': product['product_name'],  # 冗長化
                'unit_price': product['unit_price'],  # 冗長化
                'quantity': line['quantity'],
                'subtotal': product['unit_price'] * line['quantity']
            })

        # 合計金額も事前計算(集計値の冗長化)
        order_summary['total_amount'] = sum(item['subtotal'] for item in order_summary['items'])

        denormalized_view.append(order_summary)

    return denormalized_view

# 使用例:商品一覧表示用の非正規化ビュー
def create_product_display_view(products, categories, suppliers):
    """商品一覧表示用の非正規化ビュー"""
    display_view = []

    for product in products:
        category = next(c for c in categories if c['category_id'] == product['category_id'])
        supplier = next(s for s in suppliers if s['supplier_id'] == product['supplier_id'])

        # 表示用の非正規化データ
        display_view.append({
            'product_id': product['product_id'],
            'product_name': product['product_name'],
            'category_name': category['category_name'],  # 冗長化
            'supplier_name': supplier['supplier_name'],  # 冗長化
            'unit_price': product['unit_price'],
            'stock_quantity': product['stock_quantity'],
            # 検索用にキーワードも事前生成
            'search_keywords': f"{product['product_name']} {category['category_name']} {supplier['supplier_name']}"
        })

    return display_view

非正規化の原則:

  1. まずは正規化: データ整合性を確保
  2. ボトルネックを確認: パフォーマンス測定
  3. 局所的に非正規化: 必要な箇所のみ
  4. 更新トリガーで同期: 不整合を防止

正規化レベルの選択ガイド

【正規化レベルの選択基準】

| システムタイプ          | 推奨正規化 | 理由                              |
|-------------------------|------------|-----------------------------------|
| OLTP(トランザクション)| BCNF~3NF   | データ整合性が最優先              |
| OLAP(分析)            | 2NF~1NF    | 集計パフォーマンス優先(スタースキーマ) |
| Web アプリケーション    | 3NF        | 整合性と性能のバランス            |
| リアルタイムシステム    | 2NF        | 応答時間重視                      |
| バッチ処理              | 3NF~BCNF   | 整合性重視、時間は余裕あり        |
def recommend_normalization_level(system_requirements):
    """
    システム要件に基づく正規化レベルの推奨

    Parameters:
        system_requirements: システム要件 dict

    Returns:
        str: 推奨正規化レベル
    """
    read_ratio = system_requirements.get('read_ratio', 0.5)  # 読み取り割合 0-1
    consistency_critical = system_requirements.get('consistency_critical', True)
    response_time_requirement = system_requirements.get('response_time_ms', 1000)

    # 判断ロジック
    if consistency_critical and read_ratio < 0.7:
        return "BCNF - データ整合性が最重要"
    elif consistency_critical and read_ratio >= 0.7:
        return "3NF - 整合性を保ちつつ性能も考慮"
    elif read_ratio > 0.9 and response_time_requirement < 100:
        return "2NF~1NF - 読み取り性能最優先(必要に応じて非正規化)"
    else:
        return "3NF - バランス型"

# 使用例
systems = [
    {'name': '銀行勘定系', 'read_ratio': 0.3, 'consistency_critical': True, 'response_time_ms': 500},
    {'name': 'EC サイト商品一覧', 'read_ratio': 0.95, 'consistency_critical': False, 'response_time_ms': 50},
    {'name': '社内 ERP', 'read_ratio': 0.6, 'consistency_critical': True, 'response_time_ms': 2000},
    {'name': '分析ダッシュボード', 'read_ratio': 0.99, 'consistency_critical': False, 'response_time_ms': 5000},
]

print("【正規化レベル推奨】")
for sys in systems:
    level = recommend_normalization_level(sys)
    print(f"{sys['name']}: {level}")

出力:

【正規化レベル推奨】
銀行勘定系:BCNF - データ整合性が最重要
EC サイト商品一覧:2NF~1NF - 読み取り性能最優先(必要に応じて非正規化)
社内 ERP: 3NF - 整合性を保ちつつ性能も考慮
分析ダッシュボード:2NF~1NF - 読み取り性能最優先(必要に応じて非正規化)

まとめ

データベース正規化の核心:

  1. 基本概念: データ異常(挿入・更新・削除)を防止するための設計手法
  2. 第 1 正規形(1NF): 繰り返しグループを排除、各セルに 1 つの値
  3. 第 2 正規形(2NF): 部分関数従属を排除、複合キーの問題を解決
  4. 第 3 正規形(3NF): 推移的関数従属を排除、非キー属性の依存を整理
  5. BC 正規形(BCNF): 決定キーがすべて候補キー、3NF の例外を処理
  6. 実践的判断: 正規化は手段、目的は「整合性と性能の最適なバランス」
  7. 非正規化: 読み取り重視の場面では局所的な冗長化も有効

正規化は**「完璧を目指す」のではなく「目的に合わせる」**ことが重要だ。

実務での指針:

  1. 設計段階では 3NF を目指す - データ整合性の基礎を確保
  2. パフォーマンス測定后进行 - ボトルネックを特定してから最適化
  3. 非正規化は局所的に - 必要な箇所のみ、トリガーで同期
  4. ドキュメント化 - なぜ非正規化したかの理由を記録

参考資料

  • "Database System Concepts" Abraham Silberschatz 他(第 7 版)
  • "Database Design for Mere Mortals" Michael J. Hernandez 著
  • "SQL アンチパターン" Bill Karwin 著(オライリー)
  • C.J. Date "An Introduction to Database Systems"(第 8 版)
  • E.F. Codd "The Relational Model for Database Management"(1990)
  • W. Kent "A Simple Guide to Five Normal Forms in Relational Database Theory"(1983)
  • データベーススペシャリスト試験午前対策(情報処理技術者協会)

免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。