目次
リレーショナルデータベースの設計において、正規化(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
非正規化の原則:
- まずは正規化: データ整合性を確保
- ボトルネックを確認: パフォーマンス測定
- 局所的に非正規化: 必要な箇所のみ
- 更新トリガーで同期: 不整合を防止
正規化レベルの選択ガイド
【正規化レベルの選択基準】
| システムタイプ | 推奨正規化 | 理由 |
|-------------------------|------------|-----------------------------------|
| 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 正規形(1NF): 繰り返しグループを排除、各セルに 1 つの値
- 第 2 正規形(2NF): 部分関数従属を排除、複合キーの問題を解決
- 第 3 正規形(3NF): 推移的関数従属を排除、非キー属性の依存を整理
- BC 正規形(BCNF): 決定キーがすべて候補キー、3NF の例外を処理
- 実践的判断: 正規化は手段、目的は「整合性と性能の最適なバランス」
- 非正規化: 読み取り重視の場面では局所的な冗長化も有効
正規化は**「完璧を目指す」のではなく「目的に合わせる」**ことが重要だ。
実務での指針:
- 設計段階では 3NF を目指す - データ整合性の基礎を確保
- パフォーマンス測定后进行 - ボトルネックを特定してから最適化
- 非正規化は局所的に - 必要な箇所のみ、トリガーで同期
- ドキュメント化 - なぜ非正規化したかの理由を記録
参考資料
- "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)
- データベーススペシャリスト試験午前対策(情報処理技術者協会)
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。