Intermediate · SQL in depth · topic 21 of 54 · 20 min
Joins: combining tables
Explanation
Why data lives in many tables
Imagine writing the customer's name and city on every order row. If Priya places 50 orders, her city is written 50 times. When she moves, you must change 50 rows, and if you miss one the data disagrees with itself. So we store each customer once in a customers table, give the row an id, and let each order store only customer_id.
A column like orders.customer_id that points to a row in another table is a foreign key. The column it points to, customers.id, is the primary key of that table: a value that is unique for every row. With REFERENCES the database refuses an order for a customer who does not exist.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
order_date TEXT NOT NULL,
amount INTEGER NOT NULL,
status TEXT NOT NULL
);INNER JOIN: only matching rows
INNER JOIN (or just JOIN) pairs each row of the first table with the rows of the second table where the ON condition is true. Rows with no partner on the other side are dropped. The short names after the table (orders o, customers c) are aliases: they save typing and make it clear which table a column comes from.
SELECT o.id, c.name, o.amount
FROM orders AS o
INNER JOIN customers AS c ON c.id = o.customer_id
WHERE o.id <= 104
ORDER BY o.id;| o.id | c.name | o.amount |
|---|---|---|
| 101 | Aarav Sharma | 1270 |
| 102 | Priya Nair | 320 |
| 103 | Aarav Sharma | 380 |
| 104 | Rohan Gupta | 2249 |
LEFT JOIN: keep everything on the left
LEFT JOIN keeps every row of the left (first) table. When there is no matching row on the right, the right-side columns are filled with NULL, which means "no value". That makes LEFT JOIN plus WHERE ... IS NULL the classic way to find things that are missing: customers with no orders, products never sold, students who skipped an exam.
SELECT c.name, o.id AS order_id
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE c.id BETWEEN 4 AND 6
ORDER BY c.id;| c.name | order_id |
|---|---|
| Sneha Iyer | 105 |
| Imran Khan | NULL |
| Kavya Reddy | NULL |
Three tables and more
SELECT o.id, p.name, oi.qty, oi.qty * p.price AS line_total
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
JOIN products AS p ON p.id = oi.product_id
WHERE o.id = 104;| Join | Keeps |
|---|---|
| INNER JOIN | Only rows that match on both sides |
| LEFT JOIN | All rows of the left table, NULLs where the right has no match |
| RIGHT JOIN | All rows of the right table (rarely used; swap the tables and use LEFT) |
| FULL OUTER JOIN | All rows of both tables (PostgreSQL, and SQLite 3.39 or newer) |
Watch the lesson
Joins: combining tables · 1/7
Real apps never keep everything in one table. Today we learn how to put tables back together with joins.
Prefer a video by a teacher? Find free videos on “sql joins explained inner join left join tutorial” on YouTube ↗
In real life: The "My orders" page of a shopping app
When you open "My orders" in a shopping app, the backend runs one query that joins orders with order_items and products, filtered by your customer id. You see each order with product names and pictures, although the orders table itself only holds ids and amounts. The admin dashboard uses a LEFT JOIN the other way round: "customers who signed up but never ordered" is the list the marketing team uses to send a discount coupon.
SELECT o.id, o.order_date, p.name, oi.qty
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.customer_id = 2
ORDER BY o.order_date DESC;Practice (0/3 done)
Tables: customers(id, name, city) and orders(id, customer_id, order_date, amount, status). Show the order id, the customer's name and the amount for every order whose status is 'delivered', highest amount first.
Find the customers who have never placed an order. Show only their names, in alphabetical order. Use a LEFT JOIN from customers to orders.
Tables: orders(id, order_date, ...), order_items(order_id, product_id, qty) and products(id, name, category, price). For orders placed on or after 2026-03-10, list the order id, the product name and the qty. Sort by order id, then product name.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.