Demo: normalizing a library spreadsheet into tables
On This Lesson 11 topics
Lesson Overview
This lesson is a guided walkthrough. We turn one flat spreadsheet from a small library into clean tables, step by step, applying the whole module. Along the way we make one deliberate mistake, so you can see an anomaly happen.
Follow along on paper, in a spreadsheet program, or in a text file; no database software is needed (menu names differ between programs, so we describe actions rather than clicks).
The finished design is the starting point for the module lab, where you draw the ER diagram for books, authors, members, and loans and have a peer review it.
Learning Objectives
By the end of this lesson you will be able to:
- Take a flat spreadsheet through 1NF, 2NF, and 3NF in a fixed order.
- Split a list in a cell into rows, and then into a many-to-many structure.
- Demonstrate an update anomaly on real rows.
- Produce a final set of tables with primary keys, foreign keys, and column types.
- Draw the ER diagram for the result in text form and check each column against 3NF.
- Verify the new design by reconstructing the original sheet from it.
Written Explanation
The starting point
The library has kept its loans in a spreadsheet, one row per loan. The columns are the loan number, the member's name and phone, the book title, the authors (separated by semicolons), and the due date.
| loan | member | member_phone | book | authors | due_date |
|---|---|---|---|---|---|
| 1 | Layla Hassan | 0501234567 | Algorithms Made Simple | Nora Khalil; Sami Hadad | 2025-03-15 |
| 2 | Layla Hassan | 0501234567 | Databases for Beginners | Sami Hadad; Rana Youssef | 2025-03-22 |
| 3 | Omar Said | 0507654321 | Algorithms Made Simple | Nora Khalil; Sami Hadad | 2025-03-23 |
| 4 | Omar Said | 0507654321 | Emma | Jane Austen | 2025-03-30 |
The people, books, and authors are invented for the exercise.
The method
We use the procedure from the previous lesson:
- Understand what one row means and choose a key.
- Fix 1NF: no lists in cells.
- Fix 2NF: partial dependencies on a composite key.
- Fix 3NF: dependencies between non-key columns.
- Add keys and types, draw the diagram, and verify.
Scope note: the sheet says nothing about physical copies, so "a book" means a title.
Step 1: what does one row mean?
A row currently means "one loan, with all the details about the member, the book, and the authors copied in". The loan number is unique, so it is a natural starting key. Apply the "one noun per row" test from lesson L01: the row is about a loan, a member, a book, and authors all at once. That tells us to expect several tables.
Step 2: fix 1NF by splitting the authors list
The authors cell holds a list. Rows 1 to 3 each hold two names. We cannot count Sami Hadad's books or find "Hadad" reliably.
The simplest repair puts one author on each row:
| loan | member | member_phone | book | author | due_date |
|---|---|---|---|---|---|
| 1 | Layla Hassan | 0501234567 | Algorithms Made Simple | Nora Khalil | 2025-03-15 |
| 1 | Layla Hassan | 0501234567 | Algorithms Made Simple | Sami Hadad | 2025-03-15 |
| 2 | Layla Hassan | 0501234567 | Databases for Beginners | Sami Hadad | 2025-03-22 |
| 2 | Layla Hassan | 0501234567 | Databases for Beginners | Rana Youssef | 2025-03-22 |
| 3 | Omar Said | 0507654321 | Algorithms Made Simple | Nora Khalil | 2025-03-23 |
| 3 | Omar Said | 0507654321 | Algorithms Made Simple | Sami Hadad | 2025-03-23 |
| 4 | Omar Said | 0507654321 | Emma | Jane Austen | 2025-03-30 |
Every cell now has one value, so the sheet is in 1NF, but the loan number repeats and can no longer be the key; the key is now the pair (loan, author). Even the key has become awkward and the data more repetitive: a sign that we should split rather than merely reshape.
Step 3: the planned mistake (an update anomaly)
Imagine we stop here, satisfied that the lists are gone. A week later Layla gets a new phone number, 0509998888. She appears on four rows. Suppose the librarian changes the phone on the first row only: rows 2 to 4 of her entries still show 0501234567. What is Layla's phone number? The sheet answers both ways, and nothing in it says which is right. That is the update anomaly: one fact, stored in many places, changed in only some of them.
The others are visible too. Insert: a new member who has not borrowed anything cannot be entered without inventing a loan. Delete: deleting Omar's only loan would lose Omar's name and phone.
All three problems come from the same flaw: the member's facts live in the loan rows. The fix is to move each kind of fact to its own table.
Step 4: identify the entities and their facts
Look at each column and ask what it describes: loan and due_date describe the loan; member and member_phone describe the member; book describes the book; author describes the author. We have four entities (loans, members, books, authors) and a relationship, books and authors, with many on both sides. Layla's loans are a one-to-many relationship (a member has many loans), as are the loans of a book (a book appears in many loans). Both foreign keys therefore go in loans.
The 2NF and 3NF checks give the same split: member and book depend on only part of the (loan, author) key (2NF), and the phone depends on the member rather than directly on the loan (3NF). Moving each group of columns that depends on the same thing into its own table resolves both.
Step 5: build the tables
Create one table per entity, each with a generated integer id as its primary key (lesson L02), and replace each copied fact by a foreign key (lesson L03).
members(id PK, name, phone)
authors(id PK, name)
books(id PK, title)
book_authors(book_id FK -> books.id, author_id FK -> authors.id, PK(book_id, author_id))
loans(id PK, member_id FK -> members.id, book_id FK -> books.id, due_on)Choose the types (lesson L01): ids are integers; name, phone, and title are text (phone is text, because it starts with 0); due_on is a date. Mark name, title, and due_on as required.
Fill in the rows:
| members.id | name | phone |
|---|---|---|
| 1 | Layla Hassan | 0509998888 |
| 2 | Omar Said | 0507654321 |
| books.id | title |
|---|---|
| 1 | Algorithms Made Simple |
| 2 | Databases for Beginners |
| 3 | Emma |
| authors.id | name |
|---|---|
| 1 | Nora Khalil |
| 2 | Sami Hadad |
| 3 | Rana Youssef |
| 4 | Jane Austen |
| book_authors.book_id | book_authors.author_id |
|---|---|
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
| 2 | 3 |
| 3 | 4 |
| loans.id | member_id | book_id | due_on |
|---|---|---|---|
| 1 | 1 | 1 | 2025-03-15 |
| 2 | 1 | 2 | 2025-03-22 |
| 3 | 2 | 1 | 2025-03-23 |
| 4 | 2 | 3 | 2025-03-30 |
Layla's new number appears in exactly one place.
Step 6: the diagram and the check
The relationships as text, with cardinalities:
members 1 ---- * loans * ---- 1 books 1 ---- * book_authors * ---- 1 authorsRead it left to right: a member has many loans; a book is in many loans; a book has many book_authors rows; an author has many book_authors rows. In a drawing you would use five boxes and four lines, with the fork on the "many" ends.
Check against the forms. 1NF: the author list became rows in book_authors. 2NF: book_authors has no non-key columns and the other keys are single columns. 3NF: phone depends on the member, title on the book, due_on on the loan. Every FK points at a primary key.
Finally, verify by rebuilding: can you recreate the original sheet from the new tables? Take loan 3. Its member_id is 2, which gives Omar Said and his phone. Its book_id is 1, which gives "Algorithms Made Simple", and book_authors rows for book 1 give authors 1 and 2, Nora Khalil and Sami Hadad. The due date is 2025-03-23. That is row 3 of the original sheet, with no information lost. If you cannot rebuild every row, you removed something you should not have.
Examples
Example 1: a mini-demo on a simpler sheet (simple)
A club's sheet: signup(id, name, email, sport), where sport is Tennis; Chess for Aya. The list breaks 1NF, and people and sports are many-to-many:
people(id PK, name, email)
sports(id PK, name)
signups(person_id FK -> people.id, sport_id FK -> sports.id, PK(person_id, sport_id))Aya becomes one people row and two signups rows.
Example 2: spotting the anomaly in another sheet (practical)
A sheet of orders carries customer_city on every order row. Change one customer's city on one row and the sheet contradicts itself. The cure is the same as step 5: customers(id PK, name, city) and orders(id PK, customer_id FK, ordered_on). You need no knowledge of shops to run the method: find the groups of columns that describe the same thing.
Example 3: extending the library with returns and copies (realistic)
The librarian says: "We also need the date each book was returned, and we own two copies of some titles." Returns are one more fact about the loan, so loans gains returned_on (a date, empty until the book is back). Copies need a decision. If every copy is interchangeable, add a column copies_owned to books. If copies must be tracked individually, add a table, with the loan pointing at a copy instead of a title:
books(id PK, title)
copies(id PK, book_id FK -> books.id, barcode)
loans(id PK, member_id FK -> members.id, copy_id FK -> copies.id, loaned_on, due_on, returned_on)The title, authors, and book_authors stay on books, because they are facts about the title, not about one physical copy. Both designs can be 3NF; the choice depends on what the library must answer.
Backend Insight
The same sequence applies whenever a team inherits an old spreadsheet or imported file: find the groups of columns that describe the same thing, give each its own table, link them by id, and rebuild the original to verify nothing was lost. In a real project that check is done with a query that joins the new tables, which you will write after the SQL module.
Clean dirty values first: "Sami Hadad" and "S. Hadad" would become two authors.
Common Mistakes
Mistake 1: Splitting the list but not the table
- Incorrect: stopping at one row per author, with member and book details repeated.
- Correct: keep going until each kind of fact has its own table and key.
Mistake 2: Making the book title the key
- Incorrect:
books(title PK, ...), so two different books with the same title collide. - Correct: a generated
id, with the title as an ordinary column.
Mistake 3: Putting the foreign key on the wrong table
- Incorrect:
members(id, name, phone, loan_id). - Correct:
loans(..., member_id FK), the "many" side.
Exercises
Level A: Foundation
A1. Why did the 1NF step in this lesson make the loan number stop being a key?
Show solution A1
A1. Splitting the authors into rows made each loan appear on two rows, so the loan number repeats. Only the pair (loan, author) is unique.
A2. In the final design, which table holds the foreign key that links members to loans, and what is the column called?
Show solution A2
A2. loans, with the column member_id (member_id FK -> members.id).
A3. Name the three anomalies, and show one for the flat sheet.
Show solution A3
A3. Update (one fact changed in only some rows, as with Layla's phone), insert (a member cannot be entered before a loan), delete (deleting Omar's only loan loses Omar's details).
Level B: Application
B1. Normalize this sheet to 3NF and list the tables with PKs and FKs.
| student | advisor | advisor_office | courses |
|---|---|---|---|
| Lina | Dr. Mansour | B-12 | Math; Art |
| Yusuf | Dr. Mansour | B-12 | Math |
| Zaid | Dr. Farah | C-03 | Art |
Show solution B1
B1.
advisors(id PK, name, office)
students(id PK, name, advisor_id FK -> advisors.id)
courses(id PK, title)
enrollments(student_id FK -> students.id, course_id FK -> courses.id, PK(student_id, course_id))The office depends on the advisor, not on the student (3NF), and the course list is split into rows and linked by a junction table (1NF).
B2. In the final library design, write the steps to answer "which authors wrote the book borrowed in loan 3?" by following ids. Name the tables you visit.
Show solution B2
B2. Visit loans and find row 3: book_id is 1. Visit book_authors and read the rows with book_id = 1: author ids 1 and 2. Visit authors and read names: Nora Khalil and Sami Hadad.
B3. Extend the final library design so that members have a join date and loans record a return date. State the table and the type of each new column.
Show solution B3
B3. members gains joined_on (date, required). loans gains returned_on (date, optional, empty until the book is back).
Level C: Challenge
C1. A pet clinic's sheet has columns visit_id, pet_name, owner_name, owner_phone, species, vet_name, vet_phone, visit_date, treatments (a semicolon list). Normalize to 3NF with a text ER sketch. Identify which columns depend on the pet, the owner, the vet, and the visit. State your assumption about whether an owner can have many pets.
Show solution C1
C1. One possible design, assuming an owner can have many pets and each pet has one owner:
owners(id PK, name, phone)
pets(id PK, name, species, owner_id FK -> owners.id)
vets(id PK, name, phone)
treatments(id PK, name)
visits(id PK, pet_id FK -> pets.id, vet_id FK -> vets.id, visit_date)
visit_treatments(visit_id FK -> visits.id, treatment_id FK -> treatments.id, PK(visit_id, treatment_id))Owner columns depend on the owner, species on the pet, vet columns on the vet, and visit_date on the visit.
C2. Verify your C1 design by rebuilding one visit row from your tables. Then explain why two owners with the same name and phone cause no problem in your design.
Show solution C2
C2. Take visit 1: pet_id gives the pet and its owner; vet_id gives the vet; visit_treatments rows give the treatments. Each original column is reachable. Two owners with the same name and phone are two rows with different ids, which is the point of a generated key rather than the name. If they were really one person entered twice, that is a data-cleaning problem, not a design flaw.
Mini Challenge
Find or invent a spreadsheet with at least six columns and one list-like column, from your own life (a reading list, a sports team, a recipe collection). Run the six steps: say what a row means, fix 1NF, spot at least one anomaly with real example rows, find the entities, write the final tables with PKs, FKs, and types, and draw the text ER sketch. Rebuild at least two original rows from your tables to verify.
Check your work: each original column has a home, no cell holds a list, and the rebuild works.
Summary & Next Lesson
Key takeaways
- Start by asking what one row means; a row about several things signals several tables.
- Fixing a list in a cell is not enough: keep splitting until each kind of fact has its own table.
- An update anomaly appears as soon as a fact is repeated and changed in only some rows.
- The final design has a primary key per table, foreign keys on the many side, and a junction table for many-to-many.
- Always verify by rebuilding the original rows from the new tables.
Next lesson
The next step is the module lab, "Library schema": you will design the ER diagram for books, authors, members, and loans on your own, check it against 3NF, and have a peer review it.
Self-check
- In which order do we fix the normal forms?
- What is the planned mistake in this demo, and what does it show?
- Where do the foreign keys of
loanspoint? - Why is
book_authorsa separate table? - How do you verify that no information was lost?
Show answers
- 1NF, then 2NF, then 3NF.
- Keeping the member's phone on every loan row; changing it in only one row leaves two phone numbers for one member, which is the update anomaly.
member_idpoints atmembers.id, andbook_idpoints atbooks.id.- Books and authors are many-to-many, so each pairing needs its own row with two foreign keys.
- Rebuild each original row by following the ids; every original column must be reachable.
References
- Microsoft Learn, Database normalization description. Supports the normalization steps used here, including removing repeating groups and moving non-key-dependent fields into their own table, with a worked student-and-class example of the same kind.
- PostgreSQL documentation, Constraints. Supports the junction-table pattern with a composite primary key over two foreign keys, and the rule that foreign key values must match an existing row. The sample data, names, and the spreadsheet are invented for this lesson.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.