Meet the broken table — student_courses
A university database tracks which students take which courses, who teaches them, and grades. Someone crammed everything into one table. It breaks all three normal forms.
| student_id | course_id | student_name | student_city | course_name | instructor | instructor_office | grades |
|---|---|---|---|---|---|---|---|
| S01 | C01 | Ramesh | Kathmandu | Database | Mr. Sharma | Room 201 | midterm:A, final:A+ |
| S01 | C02 | Ramesh | Kathmandu | Python | Ms. Rai | Room 305 | midterm:B, final:B+ |
| S02 | C01 | Sita | Pokhara | Database | Mr. Sharma | Room 201 | midterm:A+, final:A |
| S02 | C02 | Sita | Pokhara | Python | Ms. Rai | Room 305 | midterm:B, final:A |
| S03 | C01 | Bikash | Dharan | Database | Mr. Sharma | Room 201 | midterm:C, final:B |
Primary key: (student_id, course_id) — composite key, because one student can take many courses.
grades column stores multiple values: "midterm:A, final:A+". That's a comma-separated list in a single cell, not an atomic value.
student_name and student_city depend only on student_id, not on the full (student_id, course_id) key. Same for course_name and instructor — they depend only on course_id. These are partial dependencies.
instructor_office depends on instructor, not on the primary key. Knowing the instructor tells you their office — a transitive dependency.
Fix 1NF — primary key + atomic values
1NF has two requirements: the table must have a primary key, and every cell must hold exactly one atomic value. We handle both — first identify the key, then fix the multi-valued grades column.
Part 1 — identify the primary key
Part 2 — fix the non-atomic value
Now the second 1NF rule: every cell must hold exactly one value. The grades column stores "midterm:A, final:A+" — two values crammed into one cell. You can't query a specific exam without string parsing.
| student | course | grades |
|---|---|---|
| S01 | C01 | midterm:A, final:A+ |
| S01 | C02 | midterm:B, final:B+ |
Can't query "find students who got A in the midterm" without string parsing.
| student | course | midterm_grade | final_grade |
|---|---|---|---|
| S01 | C01 | A | A+ |
| S01 | C02 | B | B+ |
Each grade is its own column — a single atomic value per cell.
grades into midterm_grade and final_grade — each holds one value. Now you can write WHERE midterm_grade = 'A' without string manipulation.grades table with rows like (S01, C01, 'midterm', 'A') would be better. For a fixed two-exam system, two columns is simpler.
Table after 1NF fix
| student_id | course_id | student_name | student_city | course_name | instructor | instructor_office | midterm_grade | final_grade |
|---|---|---|---|---|---|---|---|---|
| S01 | C01 | Ramesh | Kathmandu | Database | Mr. Sharma | Room 201 | A | A+ |
| S01 | C02 | Ramesh | Kathmandu | Python | Ms. Rai | Room 305 | B | B+ |
| S02 | C01 | Sita | Pokhara | Database | Mr. Sharma | Room 201 | A+ | A |
| S02 | C02 | Sita | Pokhara | Python | Ms. Rai | Room 305 | B | A |
| S03 | C01 | Bikash | Dharan | Database | Mr. Sharma | Room 201 | C | B |
✓ grades fixed. But the yellow and red columns still have dependency problems — let's fix those next.
Fix 2NF — remove partial dependencies
The PK is (student_id, course_id). But student_name depends only on student_id, and course_name depends only on course_id. They each depend on part of the key — not the whole key.
The partial dependencies
The fix — extract into separate tables
students. "Database" exists once in courses. Enrollments only stores what's genuinely per-enrollment: the grades.
courses table. instructor_office depends on instructor, not on course_id. If Mr. Sharma moves offices, we'd have to update every course he teaches. That's a transitive dependency — we'll fix it in the next step.
Fix 3NF — remove transitive dependencies
In the courses table, instructor_office doesn't depend on course_id — it depends on instructor. That's a transitive dependency: course_id → instructor → instructor_office.
The fix — extract instructors into their own table
| course_id | course_name | instructor | instructor_office |
|---|---|---|---|
| C01 | Database | Mr. Sharma | Room 201 |
| C02 | Python | Ms. Rai | Room 305 |
| C03 | Statistics | Mr. Sharma | Room 201 |
"Room 201" repeated for every course Mr. Sharma teaches.
courses
| course_id | course_name | instructor_id |
|---|---|---|
| C01 | Database | I01 |
| C02 | Python | I02 |
| C03 | Statistics | I01 |
instructors
| instructor_id | name | office |
|---|---|---|
| I01 | Mr. Sharma | Room 201 |
| I02 | Ms. Rai | Room 305 |
Office exists once per instructor — update one row to change it.
The final schema — 4 clean tables
From one broken table to four clean ones. Each entity has its own table. Each fact exists in exactly one place.
Test the three anomalies — all gone
instructors: UPDATE instructors SET office = 'Room 310' WHERE instructor_id = 'I01'. Every course he teaches automatically reflects the new office via JOIN. Zero duplication.INSERT INTO students VALUES ('S04', 'Anita', 'Biratnagar') — done. No enrollment needed. The student exists as an entity independently of any course.DELETE FROM enrollments WHERE student_id = 'S03' AND course_id = 'C01'. Bikash's student record survives. The Database course survives. Only the enrollment link is removed.The full journey — one summary
| Step | Problem | Fix | Tables after |
|---|---|---|---|
| 1NF | grades column has comma-separated values |
Split into midterm_grade + final_grade |
1 table |
| 2NF | student_name depends on student_id only (partial dep) | Extract students and courses tables |
3 tables |
| 3NF | instructor_office depends on instructor (transitive dep) | Extract instructors table |
4 tables |
Same pattern — apply to the rides table
The student_courses example was the practice. Your rides table is the real thing. The same dependency analysis, the same extraction pattern.
Entity hidden in the flat table: instructor
Symptom: instructor_office repeats per course
Fix: extract instructors table, reference via FK
Result: 4 tables (students, instructors, courses, enrollments)
Entity hidden in the flat table: driver, rider, location
Symptom: driver_name repeats per ride, casing splits data
Fix: extract drivers, riders, locations — reference via FK
Result: 4 tables (drivers, riders, locations, trips)