ShareYourGroupShareYourGroupSQL

Lesson 14 of 26 · see the practice database

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

idorder_datename
12026-01-05Asha Verma
22026-01-12Ravi Kumar
32026-01-20Kabir Khan
42026-02-02Asha Verma
52026-02-14Sunita Rao
62026-02-28Priya Nair
72026-03-03Ravi Kumar
82026-03-15Imran Ali
92026-03-22Neha Joshi
102026-04-01Kabir Khan
112026-04-18Vikram Das
122026-05-06Asha Verma

20 rows

Showing the first 12 of 20 rows.

Read it like this:

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: id

Put 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

idorder_datestatusnamecity
12026-01-05deliveredAsha VermaLudhiana
22026-01-12deliveredRavi KumarDelhi
32026-01-20cancelledKabir KhanMumbai
42026-02-02deliveredAsha VermaLudhiana
52026-02-14deliveredSunita RaoChennai
62026-02-28deliveredPriya NairKochi
72026-03-03deliveredRavi KumarDelhi
82026-03-15deliveredImran AliDelhi
92026-03-22cancelledNeha JoshiJaipur
102026-04-01deliveredKabir KhanMumbai
112026-04-18deliveredVikram DasKolkata
122026-05-06deliveredAsha VermaLudhiana

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

idorder_datename
22026-01-12Ravi Kumar
72026-03-03Ravi Kumar
82026-03-15Imran Ali
152026-06-21Imran Ali
162026-07-09Ravi 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_idnamequantitypriceline_total
7Notebook 200 pages1060600
7Ball Pen pack of 10290180
7Geometry Box1120120

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

nameorders
Asha Verma4
Kabir Khan3
Ravi Kumar3
Imran Ali2
Neha Joshi2
Priya Nair2
Sunita Rao2
Joy Thomas1
Vikram Das1

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

nameunits
Notebook 200 pages25
LED Bulb 9 W12
Ball Pen pack of 107
Toor Dal 1 kg6
Sunflower Oil 1 L5

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.

SELECT COUNT(*) AS rows_without_on
FROM orders, customers;

Result

rows_without_on
240

1 row

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

idname
1Asha Verma
2Ravi Kumar
3Kabir 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.

Solve it in the practice box

Show one answer
SELECT o.id, o.status, c.city FROM orders o JOIN customers c ON o.customer_id = c.id;
idstatuscity
1deliveredLudhiana
2deliveredDelhi
3cancelledMumbai
4deliveredLudhiana
5deliveredChennai
6deliveredKochi
7deliveredDelhi
8deliveredDelhi
9cancelledJaipur
10deliveredMumbai
11deliveredKolkata
12deliveredLudhiana

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.

Solve it in the practice box

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;
namequantity
Basmati Rice 5 kg2
Toor Dal 1 kg1
Notebook 200 pages4

3 rows

Exercise 3. Show the names of the customers who have a cancelled order.

Solve it in the practice box

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.