Skip to main content

πŸ”— Lesson 11: Table Relationships

Until now, all your data has lived in one or two standalone tables. Real databases have dozens β€” sometimes hundreds β€” of tables, all connected to each other. A customer places orders. An order contains books. A book belongs to categories. These connections are called relationships, and understanding them is the key to thinking like a database designer.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why data is split across multiple tables
  • Identify one-to-one, one-to-many, and many-to-many relationships
  • Read and interpret Entity-Relationship (ER) diagrams
  • Design a junction table for many-to-many relationships
  • Create related tables with proper primary and foreign key columns
  • Recognize real-world relationship patterns

Estimated Time: 45 minutes

Prerequisites: Subqueries & nested queries (Lesson 10)

πŸ“‘ In This Lesson

Why Multiple Tables?

Imagine storing everything in one giant table:

order_id customer_name customer_email customer_city book_title book_price order_date
1 Alice Johnson alice@email.com New York Dune 14.99 2026-01-15
2 Alice Johnson alice@email.com New York 1984 9.99 2026-01-15
3 Bob Smith bob@email.com Chicago Dune 14.99 2026-02-01

This works until you notice the problems:

⚠️ Problems With One Big Table

  • Data duplication β€” Alice's name, email, and city are repeated for every order she places. If she orders 50 books, that's 50 copies of the same info.
  • Update anomalies β€” If Alice changes her email, you have to update it in every row. Miss one and your data is inconsistent.
  • Deletion anomalies β€” If you delete Alice's only order, you lose her customer info entirely.
  • Insert anomalies β€” You can't add a new customer until they place an order (or you'd have NULL order fields).
  • Wasted space β€” Every row stores redundant data, bloating the table unnecessarily.

The solution: split the data into separate tables and connect them with relationships.

erDiagram customers { int id PK varchar first_name varchar last_name varchar email varchar city } orders { int id PK int customer_id FK date order_date decimal total } customers ||--o{ orders : "places"

Now Alice's info is stored once in the customers table. Her orders reference her by ID. Update her email in one place, and it's correct everywhere.

Primary Keys & Foreign Keys

Relationships depend on two key concepts you've already seen:

Primary Key (PK) β€” The Identifier

Every table should have a primary key β€” a column (or set of columns) that uniquely identifies each row. You've been using these since Lesson 4:


CREATE TABLE customers (
    id INT AUTO_INCREMENT PRIMARY KEY,   -- unique identifier
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100)
);
                

Foreign Key (FK) β€” The Connector

A foreign key is a column in one table that references the primary key of another table. It's the link that creates the relationship:


CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT,              -- foreign key
    order_date DATE,
    total DECIMAL(10, 2),
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);
                

The customer_id column in the orders table stores a value that matches an id in the customers table. This is how MySQL knows which customer placed which order.

graph LR subgraph customers["customers table"] C1["id: 1 | Alice Johnson"] C2["id: 2 | Bob Smith"] end subgraph orders["orders table"] O1["id: 1 | customer_id: 1 | 2026-01-15"] O2["id: 2 | customer_id: 1 | 2026-01-20"] O3["id: 3 | customer_id: 2 | 2026-02-01"] end C1 -.->|"referenced by"| O1 C1 -.->|"referenced by"| O2 C2 -.->|"referenced by"| O3

πŸ’‘ Naming Convention

Foreign key columns are typically named tablename_id β€” like customer_id, book_id, or order_id. This makes it immediately clear which table they reference.

βœ… Key Terminology

  • Parent table β€” the table being referenced (customers)
  • Child table β€” the table containing the foreign key (orders)
  • The foreign key in the child table must match a primary key value in the parent table (or be NULL)

One-to-Many (1:M) Relationships

The one-to-many relationship is by far the most common type in relational databases. One record in the parent table can be linked to many records in the child table, but each child record links to exactly one parent.

erDiagram customers ||--o{ orders : "places" customers { int id PK varchar first_name varchar last_name } orders { int id PK int customer_id FK date order_date }

One customer can place many orders, but each order belongs to exactly one customer.

Real-World Examples

One Side (Parent) Many Side (Child) Relationship
Customer Orders One customer places many orders
Author Books One author writes many books
Department Employees One department has many employees
Category Products One category contains many products
Teacher Classes One teacher teaches many classes
Country Cities One country has many cities

How to Build It

The foreign key always goes on the "many" side:


-- Parent table (the "one" side)
CREATE TABLE authors (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    birth_year INT
);

-- Child table (the "many" side) β€” has the foreign key
CREATE TABLE books (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    author_id INT NOT NULL,
    price DECIMAL(6, 2),
    FOREIGN KEY (author_id) REFERENCES authors(id)
);
                

πŸ’‘ How to Identify the "Many" Side

Ask yourself: "Can one [A] have multiple [B]s?" If yes, A is the "one" side and B is the "many" side. The foreign key goes in B's table.

Can one customer have multiple orders? Yes β†’ FK goes in orders.
Can one order have multiple customers? No β†’ customers is the "one" side.

One-to-One (1:1) Relationships

In a one-to-one relationship, each record in Table A is linked to exactly one record in Table B, and vice versa. These are less common and are typically used for:

  • Splitting a wide table β€” separating rarely-accessed columns to improve performance
  • Security β€” keeping sensitive data in a separate, restricted table
  • Optional data β€” information that only some records have
erDiagram customers ||--|| customer_profiles : "has" customers { int id PK varchar first_name varchar last_name varchar email } customer_profiles { int id PK int customer_id FK text bio varchar avatar_url date date_of_birth }

Implementation

A 1:1 relationship uses a foreign key with a UNIQUE constraint β€” this prevents multiple rows from pointing to the same parent:


CREATE TABLE customer_profiles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT UNIQUE NOT NULL,   -- UNIQUE enforces 1:1
    bio TEXT,
    avatar_url VARCHAR(255),
    date_of_birth DATE,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);
                

The UNIQUE constraint on customer_id ensures each customer can have at most one profile. Without it, this would be a one-to-many relationship.

Real-World Examples

Table A Table B Why Separate?
Users User Settings Optional; not all users customize settings
Employees Payroll Details Security β€” restricted access to salary data
Products Product Details Performance β€” large text/blob fields in a separate table
Countries Capital Cities Each country has exactly one capital

Many-to-Many (M:M) Relationships

In a many-to-many relationship, records on both sides can link to multiple records on the other side:

  • One book can belong to many categories (Fiction, Sci-Fi, Classic)
  • One category can contain many books

You can't represent this with a single foreign key β€” you'd need multiple values in one column, which breaks relational rules. The solution is a junction table (also called a bridge table, linking table, or join table).

The Junction Table Pattern

erDiagram books ||--o{ book_categories : "assigned to" categories ||--o{ book_categories : "contains" books { int id PK varchar title decimal price } categories { int id PK varchar name } book_categories { int book_id FK int category_id FK }

The junction table book_categories sits between the two main tables. Each row represents one link β€” "this book is in this category." The two foreign keys together form a composite primary key.

Creating the Tables


-- Table A
CREATE TABLE books (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    price DECIMAL(6, 2)
);

-- Table B
CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE
);

-- Junction table (the bridge)
CREATE TABLE book_categories (
    book_id INT NOT NULL,
    category_id INT NOT NULL,
    PRIMARY KEY (book_id, category_id),    -- composite PK
    FOREIGN KEY (book_id) REFERENCES books(id),
    FOREIGN KEY (category_id) REFERENCES categories(id)
);
                

Inserting Data


-- Add some categories
INSERT INTO categories (name) VALUES
    ('Fiction'), ('Science Fiction'), ('Classic'), ('Dystopian');

-- Link books to categories (a book can have multiple categories)
INSERT INTO book_categories (book_id, category_id) VALUES
    (1, 1),    -- Dune β†’ Fiction
    (1, 2),    -- Dune β†’ Science Fiction
    (2, 1),    -- 1984 β†’ Fiction
    (2, 4),    -- 1984 β†’ Dystopian
    (2, 3),    -- 1984 β†’ Classic
    (3, 1),    -- Pride and Prejudice β†’ Fiction
    (3, 3);    -- Pride and Prejudice β†’ Classic
                

πŸ’‘ Composite Primary Key

PRIMARY KEY (book_id, category_id) means the combination of both columns must be unique. A book can be in multiple categories, and a category can have multiple books β€” but the same book–category pair can only appear once.

Real-World Examples

Table A Table B Junction Table
Students Courses enrollments
Orders Books order_items
Actors Movies movie_cast
Users Roles user_roles
Recipes Ingredients recipe_ingredients
Playlists Songs playlist_songs

Junction Tables with Extra Data

Junction tables often carry additional data about the relationship itself:


-- Order items β€” the junction between orders and books
CREATE TABLE order_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    book_id INT NOT NULL,
    quantity INT NOT NULL DEFAULT 1,       -- extra data!
    price_at_time DECIMAL(6, 2) NOT NULL,  -- extra data!
    FOREIGN KEY (order_id) REFERENCES orders(id),
    FOREIGN KEY (book_id) REFERENCES books(id)
);
                

The quantity and price_at_time columns belong to neither the order nor the book β€” they describe the relationship between a specific order and a specific book.

βœ… Why Store Price at Time of Order?

Book prices change. If you only stored book_id and looked up the current price, historical orders would show the wrong total. Storing price_at_time captures what the customer actually paid β€” a common and important pattern.

Reading ER Diagrams

An Entity-Relationship (ER) diagram is a visual blueprint of your database. You've been seeing them throughout this lesson. Here's how to read the notation:

Crow's Foot Notation

The most common ER diagram style uses symbols at the ends of relationship lines:

Symbol Meaning Description
|| Exactly one This side has exactly one record
o| Zero or one This side has zero or one record (optional)
}| or { One or many This side has one or more records
o{ Zero or many This side has zero or more records
erDiagram CUSTOMER ||--o{ ORDER : "places" ORDER ||--|{ ORDER_ITEM : "contains" BOOK ||--o{ ORDER_ITEM : "included in" BOOK }o--o{ CATEGORY : "belongs to"

Reading this diagram:

  • A customer places zero or many orders (a new customer may have no orders yet)
  • An order contains one or many order items (an order must have at least one item)
  • A book appears in zero or many order items (some books may never be ordered)
  • A book belongs to zero or many categories, and a category contains zero or many books (many-to-many)

πŸ’‘ Reading ER Diagrams

Read each line from left to right or right to left, using the notation at each end. The verb on the line describes the relationship. Practice reading the diagram above out loud β€” "One customer places zero or many orders" β€” until it becomes natural.

Building a Bookstore Schema

Let's put it all together and design a complete schema for our bookstore. This is what we'll use for the rest of the course:

erDiagram authors ||--o{ books : "writes" books ||--o{ order_items : "ordered as" orders ||--|{ order_items : "contains" customers ||--o{ orders : "places" books }o--o{ categories : "tagged" books ||--o{ book_categories : "" categories ||--o{ book_categories : "" authors { int id PK varchar name int birth_year } books { int id PK varchar title int author_id FK decimal price int pages date published boolean in_stock } customers { int id PK varchar first_name varchar last_name varchar email varchar city } orders { int id PK int customer_id FK date order_date decimal total } order_items { int id PK int order_id FK int book_id FK int quantity decimal price_at_time } categories { int id PK varchar name } book_categories { int book_id FK int category_id FK }

The Complete SQL


-- 1. Authors (no dependencies)
CREATE TABLE authors (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    birth_year INT
);

-- 2. Books (depends on authors)
CREATE TABLE books (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    author_id INT NOT NULL,
    price DECIMAL(6, 2) NOT NULL,
    pages INT,
    published DATE,
    in_stock BOOLEAN DEFAULT TRUE,
    FOREIGN KEY (author_id) REFERENCES authors(id)
);

-- 3. Categories (no dependencies)
CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE
);

-- 4. Book-Category junction (depends on books, categories)
CREATE TABLE book_categories (
    book_id INT NOT NULL,
    category_id INT NOT NULL,
    PRIMARY KEY (book_id, category_id),
    FOREIGN KEY (book_id) REFERENCES books(id),
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- 5. Customers (no dependencies)
CREATE TABLE customers (
    id INT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE,
    city VARCHAR(50)
);

-- 6. Orders (depends on customers)
CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT NOT NULL,
    order_date DATE NOT NULL,
    total DECIMAL(10, 2) DEFAULT 0,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

-- 7. Order items (depends on orders, books)
CREATE TABLE order_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    book_id INT NOT NULL,
    quantity INT NOT NULL DEFAULT 1,
    price_at_time DECIMAL(6, 2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders(id),
    FOREIGN KEY (book_id) REFERENCES books(id)
);
                

⚠️ Creation Order Matters

Tables must be created in dependency order β€” you can't reference a table that doesn't exist yet. Create parent tables first, child tables second. The numbered comments above show a valid order.

Similarly, when dropping tables, go in reverse order β€” drop child tables first, parent tables last. Or use SET FOREIGN_KEY_CHECKS = 0; to temporarily disable the constraint checking (and re-enable with SET FOREIGN_KEY_CHECKS = 1; after).

Common Mistakes

Mistake 1: Putting the FK on the Wrong Side


-- β›” WRONG β€” FK on the "one" side
-- This says each customer can only have ONE order!
ALTER TABLE customers ADD COLUMN order_id INT;

-- βœ… CORRECT β€” FK on the "many" side
-- This allows each customer to have MANY orders
ALTER TABLE orders ADD COLUMN customer_id INT;
                

The foreign key always goes on the "many" side of a 1:M relationship.

Mistake 2: Storing Comma-Separated Values


-- β›” WRONG β€” storing multiple categories in one column
INSERT INTO books (title, categories) VALUES
    ('Dune', 'Fiction,Sci-Fi,Classic');

-- βœ… CORRECT β€” use a junction table
INSERT INTO book_categories VALUES (1, 1), (1, 2), (1, 3);
                

Never store multiple values in a single column. It makes querying, updating, and maintaining data integrity nearly impossible.

Mistake 3: Forgetting the UNIQUE Constraint for 1:1


-- β›” WRONG β€” this is actually 1:M without UNIQUE
CREATE TABLE profiles (
    id INT PRIMARY KEY,
    customer_id INT,   -- missing UNIQUE!
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

-- βœ… CORRECT β€” UNIQUE enforces 1:1
CREATE TABLE profiles (
    id INT PRIMARY KEY,
    customer_id INT UNIQUE NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);
                

Mistake 4: Creating Tables in Wrong Order


-- β›” WRONG β€” books references authors, but authors doesn't exist yet
CREATE TABLE books (
    id INT PRIMARY KEY,
    author_id INT,
    FOREIGN KEY (author_id) REFERENCES authors(id)  -- ERROR!
);
CREATE TABLE authors (id INT PRIMARY KEY, name VARCHAR(100));

-- βœ… CORRECT β€” create parent first
CREATE TABLE authors (id INT PRIMARY KEY, name VARCHAR(100));
CREATE TABLE books (
    id INT PRIMARY KEY,
    author_id INT,
    FOREIGN KEY (author_id) REFERENCES authors(id)
);
                

Exercises

πŸ‹οΈ Exercise 1: Identify the Relationship Type

For each pair, identify whether the relationship is 1:1, 1:M, or M:M:

  1. A user and their login credentials
  2. A teacher and their students
  3. A blog post and its comments
  4. A student and courses they're enrolled in
  5. A country and its capital city
  6. A doctor and their patients
βœ… Solution
  1. 1:1 β€” Each user has exactly one set of credentials, each credential belongs to one user
  2. M:M β€” One teacher can have many students, and one student can have many teachers (across different classes)
  3. 1:M β€” One blog post can have many comments, but each comment belongs to one post
  4. M:M β€” One student enrolls in many courses, and one course has many students
  5. 1:1 β€” Each country has one capital, each capital belongs to one country
  6. M:M β€” One doctor sees many patients, and one patient can see many doctors

πŸ‹οΈ Exercise 2: Design a Schema

Design the tables (just the CREATE TABLE statements) for a music streaming service with these entities:

  • Artists β€” name, country
  • Albums β€” title, release year (one artist per album)
  • Songs β€” title, duration in seconds (each song is on one album)
  • Playlists β€” name, created date
  • A song can appear on many playlists, and a playlist contains many songs
βœ… Solution

CREATE TABLE artists (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    country VARCHAR(50)
);

CREATE TABLE albums (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    artist_id INT NOT NULL,
    release_year INT,
    FOREIGN KEY (artist_id) REFERENCES artists(id)
);

CREATE TABLE songs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    album_id INT NOT NULL,
    duration_seconds INT,
    FOREIGN KEY (album_id) REFERENCES albums(id)
);

CREATE TABLE playlists (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    created_date DATE NOT NULL
);

CREATE TABLE playlist_songs (
    playlist_id INT NOT NULL,
    song_id INT NOT NULL,
    position INT,   -- track order in the playlist
    PRIMARY KEY (playlist_id, song_id),
    FOREIGN KEY (playlist_id) REFERENCES playlists(id),
    FOREIGN KEY (song_id) REFERENCES songs(id)
);
                        

πŸ‹οΈ Exercise 3: Spot the Problems

What's wrong with each of these designs?

Design A:


CREATE TABLE students (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    course_name VARCHAR(100)   -- "Math, Science, English"
);
                    

Design B:


CREATE TABLE departments (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    employee_id INT,
    FOREIGN KEY (employee_id) REFERENCES employees(id)
);
                    

Design C:


CREATE TABLE orders (
    id INT PRIMARY KEY,
    customer_id INT,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);
CREATE TABLE customers (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);
                    
βœ… Solution

Design A: Storing comma-separated values in course_name. Students and courses are M:M β€” use a junction table (enrollments) instead.

Design B: The foreign key is on the wrong side. Departments and employees are 1:M (one department has many employees). The FK should be in the employees table as department_id, not in departments.

Design C: Table creation order is wrong. The orders table references customers, but customers hasn't been created yet. Create customers first, then orders.

🎯 Quick Quiz

Question 1: In a one-to-many relationship, where does the foreign key go?

Question 2: What type of relationship requires a junction table?

Question 3: How do you enforce a one-to-one relationship?

Question 4: In our bookstore schema, what type of relationship exists between books and categories?

Summary

πŸŽ‰ Key Takeaways

  • Splitting data into multiple tables eliminates duplication and prevents update/delete/insert anomalies
  • Primary keys uniquely identify rows; foreign keys create links between tables
  • One-to-Many (1:M) β€” the most common type; FK goes on the "many" side
  • One-to-One (1:1) β€” FK with a UNIQUE constraint; used for optional or sensitive data
  • Many-to-Many (M:M) β€” requires a junction table with two foreign keys
  • Junction tables can carry extra data about the relationship (quantity, timestamp, etc.)
  • ER diagrams visualize table structures and their relationships
  • Tables must be created in dependency order (parents before children)
  • Never store comma-separated values β€” use a junction table instead

Quick Reference

Relationship Implementation Example
One-to-Many FK on the "many" side orders.customer_id β†’ customers.id
One-to-One FK + UNIQUE on child profiles.customer_id (UNIQUE) β†’ customers.id
Many-to-Many Junction table with two FKs book_categories(book_id, category_id)

πŸ“š Additional Resources

πŸš€ What's Next?

Now that you understand how tables relate to each other, it's time to make those relationships official and enforced. In the next lesson, you'll learn about foreign key constraints β€” the rules that MySQL enforces to keep your relationships consistent. What happens when you delete a customer who has orders? What if you try to create an order for a non-existent customer? Foreign keys answer these questions with ON DELETE and ON UPDATE rules.

πŸŽ‰ Congratulations!

You now think in relationships β€” one-to-one, one-to-many, many-to-many. This mental model is the foundation of every well-designed database!