Joins combine rows from multiple tables based on related columns. They're the most powerful SQL feature for working with connected data.
Table Relationships
SQL
-- One-to-Many: One user has many orders
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) UNIQUE NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
product VARCHAR(200),
amount DECIMAL(10, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Many-to-Many: Students enroll in courses
CREATE TABLE students (
id SERIAL PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE courses (
id SERIAL PRIMARY KEY,
title VARCHAR(200)
);
CREATE TABLE enrollments (
student_id INTEGER REFERENCES students(id),
course_id INTEGER REFERENCES courses(id),
grade CHAR(2),
PRIMARY KEY (student_id, course_id)
);Inner Join
Returns only rows that match in both tables:
SQL
SELECT users.name, orders.product, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
-- Result:
-- Alice | Laptop | 999.99
-- Alice | Mouse | 29.99
-- Bob | Keyboard | 79.99SQL
-- Using aliases (common pattern)
SELECT u.name, o.product, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id;
-- Join with WHERE
SELECT u.name, o.product
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;
-- Join three tables
SELECT u.name, o.product, p.category
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id;Left Join
Returns all rows from the left table, even if there's no match:
SQL
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
-- Result:
-- Alice | 2
-- Bob | 1
-- Charlie | 0 ← Even though Charlie has no ordersRight Join
Returns all rows from the right table:
SQL
SELECT u.name, o.product
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;Full Join
Returns all rows from both tables:
SQL
SELECT u.name, o.product
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;Self Join
Join a table to itself:
SQL
-- Find employees and their managers
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- Result:
-- Alice | NULL (CEO, no manager)
-- Bob | Alice
-- Charlie | Alice
-- Diana | BobCross Join
Every combination of rows from both tables:
SQL
SELECT u.name, p.product_name
FROM users u
CROSS JOIN products p;
-- Result: Every user × every product
-- (Alice, Laptop), (Alice, Mouse), (Bob, Laptop), (Bob, Mouse), ...Aggregate Functions with Joins
SQL
-- Total spending per user
SELECT
u.name,
COUNT(o.id) AS total_orders,
SUM(o.amount) AS total_spent,
AVG(o.amount) AS avg_order,
MAX(o.amount) AS largest_order,
MIN(o.amount) AS smallest_order
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
ORDER BY total_spent DESC;Subqueries in Joins
SQL
-- Users who spent more than average
SELECT u.name, SUM(o.amount) AS total_spent
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
HAVING SUM(o.amount) > (
SELECT AVG(total) FROM (
SELECT SUM(amount) AS total
FROM orders
GROUP BY user_id
) user_totals
);💡 Tip: Use INNER JOIN when you only want matching rows. Use LEFT JOIN when you want all rows from the left table regardless of matches. Avoid SELECT * with joins — always specify columns.
Next: GROUP BY and Aggregation