MySQL Table Relationships
Relational databases organize data into related tables. The power of the relational model comes from how those tables are connected. MySQL supports three fundamental relationship types — one-to-one, one-to-many, and many-to-many — each implemented through foreign keys. Choosing the right relationship model for each part of your schema is one of the most impactful design decisions you will make.
One-to-One Relationship
A one-to-one (1:1) relationship means each row in Table A corresponds to at most one row in Table B, and vice versa. You could technically put all the data in a single table, so the reason to use a 1:1 split is usually one of:
- Performance: separating rarely-accessed large columns (TEXT, BLOB) from the core row.
- Security: isolating sensitive data (SSN, payment tokens) in a table with stricter access controls.
- Optional extension: storing optional profile details separately from the required user record.
-- Core user record (read very frequently)
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
-- Extended profile (loaded only on profile page)
CREATE TABLE user_profiles (
user_id INT NOT NULL, -- FK and PK (enforces 1:1)
bio TEXT,
avatar_url VARCHAR(500),
birth_date DATE,
website VARCHAR(255),
PRIMARY KEY (user_id),
CONSTRAINT fk_profile_user
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
-- Query: join on demand
SELECT u.email, p.bio, p.avatar_url
FROM users u
LEFT JOIN user_profiles p ON u.id = p.user_id
WHERE u.id = 42;user_profiles because user_id is the primary key there.One-to-Many Relationship
A one-to-many (1:N) relationship is by far the most common pattern in relational databases. One row in the parent table can be referenced by many rows in the child table. The FK lives on the "many" side (the child).
Examples: one customer has many orders, one category has many products, one post has many comments.
-- Parent: one customer
CREATE TABLE customers (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY (id)
);
-- Child: many orders per customer
CREATE TABLE orders (
id INT NOT NULL AUTO_INCREMENT,
customer_id INT NOT NULL, -- FK on the "many" side
total DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_order_customer
FOREIGN KEY (customer_id) REFERENCES customers (id)
ON DELETE RESTRICT -- prevent deleting customers with orders
);
-- Fetch a customer's orders
SELECT o.id, o.total, o.status
FROM orders o
WHERE o.customer_id = 42
ORDER BY o.created_at DESC;
-- Count orders per customer
SELECT c.name, COUNT(o.id) AS order_count, SUM(o.total) AS lifetime_value
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY lifetime_value DESC;Many-to-Many Relationship
A many-to-many (M:N) relationship occurs when each row in Table A can relate to many rows in Table B, and vice versa. Examples: students enrolled in courses, products belonging to multiple categories, users with multiple roles.
You cannot implement M:N directly with a FK. Instead, you create a junction table (also called a bridge table, associative table, or link table) that holds one FK to each side.
-- Side A: students CREATE TABLE students ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, PRIMARY KEY (id) ); -- Side B: courses CREATE TABLE courses ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, PRIMARY KEY (id) ); -- Junction table: one row per student-course enrollment CREATE TABLE enrollments ( student_id INT NOT NULL, course_id INT NOT NULL, enrolled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, grade CHAR(2), PRIMARY KEY (student_id, course_id), -- composite PK prevents duplicates CONSTRAINT fk_enroll_student FOREIGN KEY (student_id) REFERENCES students (id) ON DELETE CASCADE, CONSTRAINT fk_enroll_course FOREIGN KEY (course_id) REFERENCES courses (id) ON DELETE CASCADE ); -- All courses for a student SELECT c.title, e.enrolled_at, e.grade FROM enrollments e JOIN courses c ON e.course_id = c.id WHERE e.student_id = 5; -- All students in a course SELECT s.name, e.enrolled_at FROM enrollments e JOIN students s ON e.student_id = s.id WHERE e.course_id = 10;
Self-Referential Relationship
A self-referential (or recursive) relationship occurs when a table references itself. This models hierarchical data such as organizational charts, category trees, threaded comments, or bill-of-materials structures.
-- Category tree: a category can have a parent category
CREATE TABLE categories (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
parent_id INT DEFAULT NULL, -- NULL for root categories
PRIMARY KEY (id),
CONSTRAINT fk_category_parent
FOREIGN KEY (parent_id) REFERENCES categories (id) ON DELETE SET NULL
);
-- Electronics -> Phones -> Smartphones (3 levels)
INSERT INTO categories (name, parent_id) VALUES ('Electronics', NULL); -- id 1
INSERT INTO categories (name, parent_id) VALUES ('Phones', 1); -- id 2
INSERT INTO categories (name, parent_id) VALUES ('Smartphones', 2); -- id 3
INSERT INTO categories (name, parent_id) VALUES ('Laptops', 1); -- id 4
-- Display hierarchy with a self-join (two levels)
SELECT
child.name AS category,
parent.name AS parent_category
FROM categories child
LEFT JOIN categories parent ON child.parent_id = parent.id;
-- Recursive query for unlimited depth (MySQL 8.0+ CTE)
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 0 AS depth
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.depth + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT CONCAT(REPEAT(' ', depth), name) AS indented_name, depth
FROM category_tree
ORDER BY depth, name;Polymorphic Associations Pattern
Polymorphic associations allow a child table to reference rows from multiple different parent tables. A classic example is a comments table that can attach to either a post or a photo or a video.
MySQL does not natively enforce polymorphic FKs. The pattern uses a target_type column alongside a target_id column.
-- Polymorphic comments: can belong to posts OR photos CREATE TABLE comments ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, target_type VARCHAR(50) NOT NULL, -- 'post', 'photo', 'video' target_id INT NOT NULL, -- ID in the respective table body TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX idx_target (target_type, target_id) -- index for lookups ); -- Insert a comment on a post INSERT INTO comments (user_id, target_type, target_id, body) VALUES (1, 'post', 42, 'Great article!'); -- Fetch all comments on post 42 SELECT c.body, c.created_at FROM comments c WHERE c.target_type = 'post' AND c.target_id = 42;
post_comments, photo_comments) for strict FK enforcement.Practical E-Commerce Schema Example
-- Complete e-commerce schema demonstrating all relationship types CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, PRIMARY KEY (id) ); -- 1:1 with users CREATE TABLE user_payment_info ( user_id INT NOT NULL, stripe_cust_id VARCHAR(50), PRIMARY KEY (user_id), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); -- 1:N with users CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', total DECIMAL(10, 2) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE RESTRICT ); CREATE TABLE products ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, price DECIMAL(10, 2) NOT NULL, stock INT NOT NULL DEFAULT 0, PRIMARY KEY (id) ); -- M:N junction: orders and products CREATE TABLE order_items ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(10, 2) NOT NULL, PRIMARY KEY (order_id, product_id), FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT ); CREATE TABLE categories ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, parent_id INT DEFAULT NULL, -- self-referential 1:N PRIMARY KEY (id), FOREIGN KEY (parent_id) REFERENCES categories (id) ON DELETE SET NULL ); -- M:N: products can belong to multiple categories CREATE TABLE product_categories ( product_id INT NOT NULL, category_id INT NOT NULL, PRIMARY KEY (product_id, category_id), FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE );
Quick Reference
Relationship | FK Location | Example |
|---|---|---|
One-to-One | FK + PK on child table | users -> user_profiles |
One-to-Many | FK on the "many" (child) table | customers -> orders |
Many-to-Many | Junction table with two FKs | students <-> courses via enrollments |
Self-referential | FK references own PK | categories.parent_id -> categories.id |
Polymorphic | type + id columns (no DB FK) | comments -> posts or photos |