Joining two tables
So far every query read one table. But the facts you want are usually spread over several. The orders table says that order 7 was placed by customer 2. It does not say who customer 2 is. That is in customers. A join brings the two together.
Why the data is split up
A good database stores each fact once. A customer's name and city are kept in one row of customers. Each order carries only the customer's number. If the name were copied into every order, a change of name would have to be made in many places, and sooner or later one would be missed.
The column orders.customer_id holds a value from customers.id. Such a column is called a foreign key: it points to the primary key of another table. Joins follow these pointers.
The first join
SELECT orders.id, orders.order_date, customers.name
FROM orders
JOIN customers ON orders.customer_id = customers.id;Result
| id | order_date | name |
|---|---|---|
| 1 | 2026-01-05 | Asha Verma |
| 2 | 2026-01-12 | Ravi Kumar |
| 3 | 2026-01-20 | Kabir Khan |
| 4 | 2026-02-02 | Asha Verma |
| 5 | 2026-02-14 | Sunita Rao |
| 6 | 2026-02-28 | Priya Nair |
| 7 | 2026-03-03 | Ravi Kumar |
| 8 | 2026-03-15 | Imran Ali |
| 9 | 2026-03-22 | Neha Joshi |
| 10 | 2026-04-01 | Kabir Khan |
| 11 | 2026-04-18 | Vikram Das |
| 12 | 2026-05-06 | Asha Verma |
20 rows
Showing the first 12 of 20 rows.
Read it like this:
FROM ordersstarts with the orders.JOIN customersbrings in the customers table.ON orders.customer_id = customers.idis the matching rule. For each order, find the customer whoseidequals the order'scustomer_id.
Each row of the result is an order with its customer's details glued on. JOIN on its own means INNER JOIN. You can write either.
Table names in front of columns
Both tables have a column called id. Writing just id would be unclear, and the database would refuse.
SELECT id, order_date, name
FROM orders
JOIN customers ON orders.customer_id = customers.id;What the database says
Error: ambiguous column name: idPut the table name and a dot in front: orders.id. It is only required when the name exists in both tables, but in a join it is good manners to do it for every column, so that a reader can see where each one comes from.
Short names for tables
Typing full table names gets long. Give each table a short alias after its name and use that instead.
SELECT o.id, o.order_date, o.status, c.name, c.city
FROM orders AS o
JOIN customers AS c ON o.customer_id = c.id;Result
| id | order_date | status | name | city |
|---|---|---|---|---|
| 1 | 2026-01-05 | delivered | Asha Verma | Ludhiana |
| 2 | 2026-01-12 | delivered | Ravi Kumar | Delhi |
| 3 | 2026-01-20 | cancelled | Kabir Khan | Mumbai |
| 4 | 2026-02-02 | delivered | Asha Verma | Ludhiana |
| 5 | 2026-02-14 | delivered | Sunita Rao | Chennai |
| 6 | 2026-02-28 | delivered | Priya Nair | Kochi |
| 7 | 2026-03-03 | delivered | Ravi Kumar | Delhi |
| 8 | 2026-03-15 | delivered | Imran Ali | Delhi |
| 9 | 2026-03-22 | cancelled | Neha Joshi | Jaipur |
| 10 | 2026-04-01 | delivered | Kabir Khan | Mumbai |
| 11 | 2026-04-18 | delivered | Vikram Das | Kolkata |
| 12 | 2026-05-06 | delivered | Asha Verma | Ludhiana |
20 rows
Showing the first 12 of 20 rows.
The word AS is optional here, and most people leave it out: FROM orders o JOIN customers c. One- or two-letter aliases taken from the table name are the custom.
Adding WHERE and ORDER BY
A joined result behaves like one wide table. Everything you already know works on it.
SELECT o.id, o.order_date, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.city = 'Delhi'
ORDER BY o.order_date;Result
| id | order_date | name |
|---|---|---|
| 2 | 2026-01-12 | Ravi Kumar |
| 7 | 2026-03-03 | Ravi Kumar |
| 8 | 2026-03-15 | Imran Ali |
| 15 | 2026-06-21 | Imran Ali |
| 16 | 2026-07-09 | Ravi Kumar |
5 rows
The WHERE here tests a column from customers, and the result shows orders. That is the whole point of joining: filter by one table, show from another.
Another pair of tables
order_items links to products in the same way.
SELECT oi.order_id, p.name, oi.quantity, p.price,
oi.quantity * p.price AS line_total
FROM order_items oi
JOIN products p ON oi.product_id = p.id
WHERE oi.order_id = 7;Result
| order_id | name | quantity | price | line_total |
|---|---|---|---|---|
| 7 | Notebook 200 pages | 10 | 60 | 600 |
| 7 | Ball Pen pack of 10 | 2 | 90 | 180 |
| 7 | Geometry Box | 1 | 120 | 120 |
3 rows
The calculation uses a column from each table. This is a bill for order 7.
Joining and grouping
Join first, then group. Lesson 12 counted orders for each customer_id. Now the names can be shown.
SELECT c.name, COUNT(*) AS orders
FROM orders o
JOIN customers c ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY orders DESC, c.name;Result
| name | orders |
|---|---|
| Asha Verma | 4 |
| Kabir Khan | 3 |
| Ravi Kumar | 3 |
| Imran Ali | 2 |
| Neha Joshi | 2 |
| Priya Nair | 2 |
| Sunita Rao | 2 |
| Joy Thomas | 1 |
| Vikram Das | 1 |
9 rows
Grouping by c.id as well as c.name is a careful habit. Two different customers could have the same name, and grouping by name alone would merge them into one.
Units sold of each product, by name:
SELECT p.name, SUM(oi.quantity) AS units
FROM order_items oi
JOIN products p ON oi.product_id = p.id
GROUP BY p.id, p.name
ORDER BY units DESC
LIMIT 5;Result
| name | units |
|---|---|
| Notebook 200 pages | 25 |
| LED Bulb 9 W | 12 |
| Ball Pen pack of 10 | 7 |
| Toor Dal 1 kg | 6 |
| Sunflower Oil 1 L | 5 |
5 rows
What an inner join leaves out
An inner join returns only the rows that have a match on both sides. Customers who never ordered do not appear in a join of orders and customers. A product that nobody bought does not appear in a join of order items and products.
SELECT COUNT(DISTINCT c.id) AS customers_in_the_join
FROM orders o
JOIN customers c ON o.customer_id = c.id;Result
| customers_in_the_join |
|---|
| 9 |
1 row
There are twelve customers in the table and fewer in the join. Sometimes that is what you want. When it is not, you need a LEFT JOIN, the next lesson.
Forgetting the ON
Without a matching rule, every row of the first table is paired with every row of the second. Twenty orders times twelve customers gives 240 rows of nonsense.
This is called a cross join or a Cartesian product. On two large tables it produces millions of rows and can stall a server. If a join returns far more rows than you expected, look at the ON first.
The old comma style
You will meet older queries that list the tables with commas and put the matching rule in WHERE:
SELECT o.id, c.name
FROM orders o, customers c
WHERE o.customer_id = c.id
LIMIT 3;Result
| id | name |
|---|---|
| 1 | Asha Verma |
| 2 | Ravi Kumar |
| 3 | Kabir Khan |
3 rows
It gives the same result. The JOIN ... ON style is better, because the matching rule sits next to the table it belongs to and cannot be forgotten among the other conditions.
How many rows will a join return?
Think about it before running the query. Each order has exactly one customer, so joining orders to customers returns one row per order: twenty. Each order has several items, so joining orders to order items returns one row per item. Knowing what to expect is how you notice a wrong join.
Try it yourself
Exercise 1. Show each order's id and status together with the city of the customer who placed it.
Show one answer
SELECT o.id, o.status, c.city FROM orders o JOIN customers c ON o.customer_id = c.id;| id | status | city |
|---|---|---|
| 1 | delivered | Ludhiana |
| 2 | delivered | Delhi |
| 3 | cancelled | Mumbai |
| 4 | delivered | Ludhiana |
| 5 | delivered | Chennai |
| 6 | delivered | Kochi |
| 7 | delivered | Delhi |
| 8 | delivered | Delhi |
| 9 | cancelled | Jaipur |
| 10 | delivered | Mumbai |
| 11 | delivered | Kolkata |
| 12 | delivered | Ludhiana |
20 rows
Showing the first 12 of 20 rows.
Exercise 2. Show the name of each product in order number 1, with the quantity ordered.
Show one answer
SELECT p.name, oi.quantity FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = 1;| name | quantity |
|---|---|
| Basmati Rice 5 kg | 2 |
| Toor Dal 1 kg | 1 |
| Notebook 200 pages | 4 |
3 rows
Exercise 3. Show the names of the customers who have a cancelled order.
Show one answer
SELECT c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.status = 'cancelled';| name |
|---|
| Kabir Khan |
| Neha Joshi |
2 rows
Examples on this page were run with SQLite 3.50.4 on 12 November 2026. The results shown are the real results.
This is an independent learning site. It is not connected with the makers of any database product. MySQL, PostgreSQL, SQL Server, Oracle and SQLite are named only so that you know where a piece of SQL works. The shop, its customers and its staff in the practice database are made up.