Skip to content
Ameen Abozaid
PublishedBEG-049-L06

Demo: normalizing a library spreadsheet into tables

  • Lesson 6 of 6
  • 13 min read
  • Beginner
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.

loanmembermember_phonebookauthorsdue_date
1Layla Hassan0501234567Algorithms Made SimpleNora Khalil; Sami Hadad2025-03-15
2Layla Hassan0501234567Databases for BeginnersSami Hadad; Rana Youssef2025-03-22
3Omar Said0507654321Algorithms Made SimpleNora Khalil; Sami Hadad2025-03-23
4Omar Said0507654321EmmaJane Austen2025-03-30

The people, books, and authors are invented for the exercise.

The method

We use the procedure from the previous lesson:

  1. Understand what one row means and choose a key.
  2. Fix 1NF: no lists in cells.
  3. Fix 2NF: partial dependencies on a composite key.
  4. Fix 3NF: dependencies between non-key columns.
  5. 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:

loanmembermember_phonebookauthordue_date
1Layla Hassan0501234567Algorithms Made SimpleNora Khalil2025-03-15
1Layla Hassan0501234567Algorithms Made SimpleSami Hadad2025-03-15
2Layla Hassan0501234567Databases for BeginnersSami Hadad2025-03-22
2Layla Hassan0501234567Databases for BeginnersRana Youssef2025-03-22
3Omar Said0507654321Algorithms Made SimpleNora Khalil2025-03-23
3Omar Said0507654321Algorithms Made SimpleSami Hadad2025-03-23
4Omar Said0507654321EmmaJane Austen2025-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).

text
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.idnamephone
1Layla Hassan0509998888
2Omar Said0507654321
books.idtitle
1Algorithms Made Simple
2Databases for Beginners
3Emma
authors.idname
1Nora Khalil
2Sami Hadad
3Rana Youssef
4Jane Austen
book_authors.book_idbook_authors.author_id
11
12
22
23
34
loans.idmember_idbook_iddue_on
1112025-03-15
2122025-03-22
3212025-03-23
4232025-03-30

Layla's new number appears in exactly one place.

Step 6: the diagram and the check

The relationships as text, with cardinalities:

text
members 1 ---- * loans * ---- 1 books 1 ---- * book_authors * ---- 1 authors

Read 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:

text
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:

text
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.

studentadvisoradvisor_officecourses
LinaDr. MansourB-12Math; Art
YusufDr. MansourB-12Math
ZaidDr. FarahC-03Art
Show solution B1

B1.

text
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:

text
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

  1. In which order do we fix the normal forms?
  2. What is the planned mistake in this demo, and what does it show?
  3. Where do the foreign keys of loans point?
  4. Why is book_authors a separate table?
  5. How do you verify that no information was lost?
Show answers
  1. 1NF, then 2NF, then 3NF.
  2. 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.
  3. member_id points at members.id, and book_id points at books.id.
  4. Books and authors are many-to-many, so each pairing needs its own row with two foreign keys.
  5. 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.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.