Third Normal Form (3NF)
3NF Examples
Read aloud is unavailable in this browser.
Third Normal Form in Practice
Theory becomes mastery through practice. This final page applies everything we've learned about Third Normal Form—transitive dependencies, the formal definition, violation identification, and the synthesis algorithm—to real-world database design scenarios.
We'll work through complete examples from multiple business domains, showing the full journey from an unnormalized "universal relation" through analysis to a properly decomposed 3NF schema. These examples demonstrate how 3NF principles translate into practical database designs that eliminate redundancy while remaining efficient and maintainable.
By the end of this page, you will have seen 3NF applied to e-commerce, healthcare, education, and enterprise HR domains. You'll understand how to analyze real business requirements, identify functional dependencies, and produce clean 3NF decompositions that serve actual application needs.
Example 1: E-Commerce Order Management
Scenario: An online retailer needs a database to track orders, customers, products, and shipping.
Initial Universal Relation:
OrderData(OrderID, OrderDate, CustomerID, CustomerName, CustomerEmail, CustomerAddress, ProductID, ProductName, ProductCategory, CategoryDiscount, Quantity, UnitPrice, ShipperID, ShipperName, ShipperPhone, TrackingNumber)
| OrderID | CustomerID | CustomerName | ProductID | ProductName | ProductCategory | CategoryDiscount | ShipperID | ShipperName |
|---|---|---|---|---|---|---|---|---|
| 1001 | C100 | Alice Chen | P001 | Laptop Pro | Electronics | 5% | S01 | FastShip |
| 1001 | C100 | Alice Chen | P002 | Mouse Pad | Accessories | 10% | S01 | FastShip |
| 1002 | C100 | Alice Chen | P003 | USB Cable | Electronics | 5% | S02 | QuickDeliver |
| 1003 | C101 | Bob Smith | P001 | Laptop Pro | Electronics | 5% | S01 | FastShip |
Functional Dependencies Identified:
F = {
OrderID → CustomerID, OrderDate, ShipperID, TrackingNumber
CustomerID → CustomerName, CustomerEmail, CustomerAddress
ProductID → ProductName, ProductCategory, UnitPrice
ProductCategory → CategoryDiscount
ShipperID → ShipperName, ShipperPhone
{OrderID, ProductID} → Quantity
}
Candidate Key Analysis:
{OrderID, ProductID}⁺ = all attributes (one order can have multiple products)
Candidate Key: {OrderID, ProductID}
3NF Violations Identified
Applying the determinant test
- •OrderID → CustomerID, ShipperID, ...: OrderID is not a superkey (missing ProductID)
- •CustomerID is non-prime ❌ VIOLATION
- •CustomerID → CustomerName, CustomerEmail, CustomerAddress: CustomerID not superkey
- •All dependents are non-prime ❌ VIOLATION
- •ProductID → ProductName, ProductCategory, UnitPrice: ProductID not superkey
- •All dependents are non-prime ❌ VIOLATION
- •ProductCategory → CategoryDiscount: ProductCategory not superkey
- •CategoryDiscount is non-prime ❌ VIOLATION (2-level transitive!)
- •ShipperID → ShipperName, ShipperPhone: ShipperID not superkey
- •All dependents are non-prime ❌ VIOLATION
Applying 3NF Synthesis Algorithm:
Step 1: Canonical Cover (simplified—combining same left sides)
Fc = {
OrderID → CustomerID, OrderDate, ShipperID, TrackingNumber
CustomerID → CustomerName, CustomerEmail, CustomerAddress
ProductID → ProductName, ProductCategory, UnitPrice
ProductCategory → CategoryDiscount
ShipperID → ShipperName, ShipperPhone
{OrderID, ProductID} → Quantity
}
Step 2: Create Relations
| Relation | Attributes | Primary Key |
|---|---|---|
| Orders | OrderID, CustomerID, OrderDate, ShipperID, TrackingNumber | OrderID |
| Customers | CustomerID, CustomerName, CustomerEmail, CustomerAddress | CustomerID |
| Products | ProductID, ProductName, ProductCategory, UnitPrice | ProductID |
| Categories | ProductCategory, CategoryDiscount | ProductCategory |
| Shippers | ShipperID, ShipperName, ShipperPhone | ShipperID |
| OrderItems | OrderID, ProductID, Quantity | {OrderID, ProductID} |
Step 3: OrderItems contains candidate key {OrderID, ProductID} ✓
Step 4: No subsumptions to remove.
The 3NF design eliminates all anomalies: Customer info stored once (no update anomalies), new categories can exist without products (no insert anomalies), deleting last product in category keeps category info (no delete anomalies).
Example 2: Healthcare Patient Records
Scenario: A hospital needs to track patient visits, doctors, diagnoses, and treatments.
Initial Universal Relation:
PatientVisit(VisitID, VisitDate, PatientID, PatientName, PatientDOB, InsuranceID, InsuranceName, InsuranceCoverage, DoctorID, DoctorName, DoctorSpecialty, SpecialtyDept, DiagnosisCode, DiagnosisName, TreatmentID, TreatmentName, TreatmentCost)
Business Rules (Functional Dependencies):
F = {
VisitID → PatientID, VisitDate, DoctorID
PatientID → PatientName, PatientDOB, InsuranceID
InsuranceID → InsuranceName, InsuranceCoverage
DoctorID → DoctorName, DoctorSpecialty
DoctorSpecialty → SpecialtyDept
DiagnosisCode → DiagnosisName
TreatmentID → TreatmentName, TreatmentCost
{VisitID, DiagnosisCode} → TreatmentID (each diagnosis in a visit has one treatment)
}
Candidate Key: {VisitID, DiagnosisCode} — A visit can have multiple diagnoses, each with its own treatment.
Transitive Dependency Chains
Multiple levels of transitivity
- •Chain 1: VisitID → PatientID → InsuranceID → InsuranceName
- •Three levels deep! InsuranceName transitively depends on VisitID
- •
- •Chain 2: VisitID → DoctorID → DoctorSpecialty → SpecialtyDept
- •SpecialtyDept is three levels from VisitID
- •
- •Chain 3: {VisitID, DiagnosisCode} → TreatmentID → TreatmentName
- •TreatmentName transitively depends on key through TreatmentID
- •
- •Each chain indicates embedded entities: Insurance within Patient,
- •Department within Specialty within Doctor, Treatment within VisitDiagnosis
3NF Decomposition Result:
| Relation | Purpose | Primary Key | Foreign Keys |
|---|---|---|---|
| Visits | Core visit records | VisitID | PatientID, DoctorID |
| Patients | Patient demographics | PatientID | InsuranceID |
| Insurance | Insurance providers | InsuranceID | — |
| Doctors | Doctor information | DoctorID | DoctorSpecialty |
| Specialties | Medical specialties | DoctorSpecialty | — |
| Diagnoses | Diagnosis codes | DiagnosisCode | — |
| Treatments | Treatment options | TreatmentID | — |
| VisitDiagnoses | Visit-diagnosis-treatment | {VisitID, DiagnosisCode} | TreatmentID |
| Relation | Attributes | Each Row Represents |
|---|---|---|
| Visits | VisitID, VisitDate, PatientID, DoctorID | One hospital visit |
| Patients | PatientID, PatientName, PatientDOB, InsuranceID | One patient |
| Insurance | InsuranceID, InsuranceName, InsuranceCoverage | One insurance provider |
| Doctors | DoctorID, DoctorName, DoctorSpecialty | One doctor |
| Specialties | DoctorSpecialty, SpecialtyDept | One medical specialty |
| Diagnoses | DiagnosisCode, DiagnosisName | One diagnosis type |
| Treatments | TreatmentID, TreatmentName, TreatmentCost | One treatment type |
| VisitDiagnoses | VisitID, DiagnosisCode, TreatmentID | One diagnosis in one visit |
Notice how each real-world entity (Patient, Doctor, Insurance, etc.) becomes its own table. This is the essence of good normalization—store facts about each entity exactly once, in a table dedicated to that entity type.
Example 3: University Course Registration
Scenario: A university tracks courses, sections, instructors, students, and enrollments.
Initial Universal Relation:
Registration(StudentID, StudentName, StudentMajor, MajorDept, CourseID, CourseName, CourseCredits, SectionID, Semester, InstructorID, InstructorName, InstructorOffice, RoomNumber, RoomCapacity, Grade)
Functional Dependencies:
F = {
StudentID → StudentName, StudentMajor
StudentMajor → MajorDept
CourseID → CourseName, CourseCredits
{CourseID, SectionID, Semester} → InstructorID, RoomNumber
InstructorID → InstructorName, InstructorOffice
RoomNumber → RoomCapacity
{StudentID, CourseID, SectionID, Semester} → Grade
}
Candidate Key Analysis:
{StudentID, CourseID, SectionID, Semester}⁺ = all attributes
Candidate Key: {StudentID, CourseID, SectionID, Semester}
3NF Synthesis Step-by-Step
Complete algorithm walkthrough
- •Step 1: Canonical Cover Fc (combining same LHS)
- •StudentID → StudentName, StudentMajor
- •StudentMajor → MajorDept
- •CourseID → CourseName, CourseCredits
- •{CourseID, SectionID, Semester} → InstructorID, RoomNumber
- •InstructorID → InstructorName, InstructorOffice
- •RoomNumber → RoomCapacity
- •{StudentID, CourseID, SectionID, Semester} → Grade
- •
- •Step 2: Create Relations
- •R1: Students(StudentID, StudentName, StudentMajor)
- •R2: Majors(StudentMajor, MajorDept)
- •R3: Courses(CourseID, CourseName, CourseCredits)
- •R4: Sections(CourseID, SectionID, Semester, InstructorID, RoomNumber)
- •R5: Instructors(InstructorID, InstructorName, InstructorOffice)
- •R6: Rooms(RoomNumber, RoomCapacity)
- •R7: Enrollments(StudentID, CourseID, SectionID, Semester, Grade)
- •
- •Step 3: Check Key Presence
- •R7 contains {StudentID, CourseID, SectionID, Semester} ✓
- •
- •Step 4: No subsumptions
This 7-table design might seem complex, but each table has a clear purpose and stores each fact once. Adding a new major, changing an instructor's office, or updating a room's capacity each requires updating exactly one row in one table.
Example 4: Enterprise Human Resources
Scenario: A corporation tracks employees, departments, projects, skills, and assignments.
Initial Universal Relation:
Workforce(EmpID, EmpName, EmpAddress, DeptID, DeptName, DeptLocation, ManagerID, ManagerName, ProjectID, ProjectName, ProjectBudget, ProjectDeptID, SkillID, SkillName, SkillCategory, CategoryDesc, HoursWorked, SkillLevel)
Business Rules & Functional Dependencies:
F = {
EmpID → EmpName, EmpAddress, DeptID
DeptID → DeptName, DeptLocation, ManagerID
ManagerID → ManagerName
ProjectID → ProjectName, ProjectBudget, ProjectDeptID
SkillID → SkillName, SkillCategory
SkillCategory → CategoryDesc
{EmpID, ProjectID} → HoursWorked
{EmpID, SkillID} → SkillLevel
}
Note: This example has two types of many-to-many relationships:
- Employees work on Projects (with HoursWorked)
- Employees have Skills (with SkillLevel)
Identifying Multiple Transitive Chains
Complex dependency structure
- •Employee-Department-Manager Chain:
- •EmpID → DeptID → ManagerID → ManagerName
- •Violation: DeptID → ManagerID (DeptID not superkey, ManagerID non-prime)
- •Violation: ManagerID → ManagerName (ManagerID not superkey)
- •
- •Project Chain:
- •ProjectID → ProjectDeptID (implies Dept info)
- •Violation: ProjectID not superkey, ProjectDeptID non-prime
- •
- •Skill-Category Chain:
- •SkillID → SkillCategory → CategoryDesc
- •Violation: SkillCategory → CategoryDesc (Category not superkey)
- •
- •Key Insight: ManagerName appears because Manager is an Employee!
- •This suggests self-referencing: Manager IS an Employee
3NF Decomposition:
| Relation | Attributes | Key | Notes |
|---|---|---|---|
| Employees | EmpID, EmpName, EmpAddress, DeptID | EmpID | Core employee data |
| Departments | DeptID, DeptName, DeptLocation, ManagerID | DeptID | ManagerID references Employees |
| Projects | ProjectID, ProjectName, ProjectBudget, ProjectDeptID | ProjectID | ProjectDeptID references Departments |
| Skills | SkillID, SkillName, SkillCategory | SkillID | SkillCategory references Categories |
| SkillCategories | SkillCategory, CategoryDesc | SkillCategory | Lookup table |
| ProjectAssignments | EmpID, ProjectID, HoursWorked | {EmpID, ProjectID} | M:N junction |
| EmployeeSkills | EmpID, SkillID, SkillLevel | {EmpID, SkillID} | M:N junction |
Special Design Note — Self-Referencing:
Notice that we do NOT create a separate Managers table. Since managers are employees, we use a self-referencing foreign key:
CREATE TABLE Departments (
DeptID INT PRIMARY KEY,
DeptName VARCHAR(100),
DeptLocation VARCHAR(100),
ManagerID INT REFERENCES Employees(EmpID)
);
This avoids duplicating employee information for managers—ManagerName comes from joining Departments with Employees on ManagerID.
When you see ManagerID → ManagerName alongside EmpID → EmpName with the same name pattern, it's a strong hint that Manager is just a role played by an Employee. Don't create redundant entity tables for roles—use foreign keys.
Common 3NF Design Patterns
After seeing multiple examples, clear patterns emerge in 3NF database design. Understanding these patterns accelerates future designs.
- •What it is: A table for each real-world entity type (Customer, Product, Employee, etc.)
- •Structure: Entity ID as primary key, descriptive attributes as columns
- •Created by: FDs of form EntityID → Attribute1, Attribute2, ...
- •Example: Customers(CustomerID, CustomerName, CustomerEmail, CustomerAddress)
- •What it is: A table for code-to-description mappings
- •Structure: Code as primary key, description/properties as columns
- •Created by: FDs of form Code → Description
- •Example: SkillCategories(SkillCategory, CategoryDesc)
- •What it is: A table implementing many-to-many relationships
- •Structure: Composite key of both entity IDs, plus relationship attributes
- •Created by: FDs of form {Entity1ID, Entity2ID} → RelationshipAttribute
- •Example: OrderItems(OrderID, ProductID, Quantity) or EmployeeSkills(EmpID, SkillID, SkillLevel)
- •What it is: A table where entities reference other entities of the same type
- •Structure: Entity ID primary key, with foreign key to same table
- •Created by: Recognizing that roles (Manager, Supervisor) are played by base entities
- •Example: Employees(EmpID, EmpName, ManagerID FK→EmpID)
| If You See... | It's Pattern | Action |
|---|---|---|
| EntityID → multiple attributes | Entity Table | Create table EntityID PK → attributes |
| Code → Description | Lookup Table | Create table Code PK → Description |
| {ID1, ID2} → attribute | Junction Table | Create table {ID1, ID2} PK → attribute |
| ParentID → ParentName (same type as main entity) | Self-Reference | Add FK column to same table, don't create new table |
Before & After: The Impact of 3NF
Let's quantify the benefits of 3NF normalization by comparing before and after scenarios.
- •Storage: Customer address stored with every order
- •Update Cost: Change address → update 100 orders
- •Insert Problem: Can't add customer without order
- •Delete Problem: Cancel all orders → lose customer
- •Consistency Risk: High (100 places to update)
- •Query Complexity: Simple (everything in one table)
- •Storage: Customer address stored once
- •Update Cost: Change address → update 1 row
- •Insert: Add customer anytime (separate table)
- •Delete: Cancel orders → customer remains
- •Consistency Risk: Low (1 place to update)
- •Query Complexity: Moderate (joins required)
Trade-off Analysis:
| Metric | Unnormalized | 3NF | Winner |
|---|---|---|---|
| Storage Space | High (redundant) | Optimal | 3NF |
| Write Performance | Complex (multi-row) | Simple (single-row) | 3NF |
| Read Performance | Fast (no joins) | Moderate (joins needed) | Unorm* |
| Data Integrity | Low | High | 3NF |
| Maintenance Effort | High | Low | 3NF |
* The read performance advantage of unnormalized design is often overstated. Modern databases optimize joins efficiently, and caching can address hot paths. The maintenance and integrity costs of redundancy usually outweigh query performance gains.
3NF optimizes for data integrity and maintainability—the properties that matter most in transactional systems. For read-heavy analytics, you might denormalize strategically (covered in Denormalization chapter). Start with 3NF; denormalize only when needed with clear justification.
3NF Verification Checklist
After completing a 3NF decomposition, use this checklist to verify correctness:
- •□ Every table has a clear primary key — No table without a key; no multi-value cells
- •□ Every non-key column depends on the whole key — No partial dependencies (2NF satisfied)
- •□ No non-key column depends on another non-key column — No transitive dependencies (3NF satisfied)
- •□ All original FDs can be verified in one table — Dependency preservation achieved
- •□ Joining all tables reproduces original data — Lossless join achieved
- •□ No redundant tables — No table is subset of another
- •□ Each table represents one entity type — Clear semantic meaning
- •□ Foreign keys properly reference primary keys — Referential integrity possible
Quick Verification Technique:
For each table in your decomposition, perform these rapid checks:
-
State what entity type each row represents (if unclear, table may be mixing entities)
-
For each non-key attribute, ask: "Does this describe the entity, or some other related entity?" (if other entity, split needed)
-
Try to construct an anomaly scenario: Can you update, insert, or delete in a way that creates inconsistency? (if yes, violation remains)
-
Write the original FDs and check each one: Can you verify each FD using only one table in your decomposition? (if no, dependency not preserved)
Don't assume the synthesis algorithm output is perfect without verification. Check for: (1) missed FDs from business rules, (2) incorrectly identified keys, (3) accidentally removed needed tables during subsumption step. The algorithm is correct, but human input errors can corrupt results.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.