SQL Tutorial › Joins & Set Operators › INNER JOIN

SQL INNER JOIN

IntermediateLesson 32 of 6912 min read6 queries run on MySQL 8.4

INNER JOIN returns the rows that have a match in both tables. It is by far the most used join: orders with their customers, order lines with their products, employees with their departments. This lesson builds inner joins step by step on the sample shop data, from two tables to four.

Advertisement
In this lesson
  • Write an INNER JOIN with ON
  • Use table aliases and qualified column names
  • Join three and four tables
  • Combine joins with WHERE, GROUP BY and USING

INNER JOIN syntax

MySQL
SELECT columnsFROM table1 t1INNER JOIN table2 t2 ON t2.key = t1.key;

INNER is optional: JOIN alone means INNER JOIN.

Sample data

orders · 10 rows
order_idcustomer_idorder_datestatus
100112025-01-05Delivered
100222025-01-17Delivered
100312025-02-02Delivered
100432025-02-14Cancelled
100542025-03-08Delivered
100652025-03-22Shipped
100722025-04-10Delivered
100812025-04-28Pending
100932025-05-15Delivered
101052025-06-01Delivered
customers · 7 rows (some columns)
customer_idcustomer_namecity
1Ananya SharmaMumbai
2Rohit KumarDelhi
3Fatima KhanHyderabad
4David FernandesGoa
5Ishita BoseKolkata
6Manoj PillaiChennai
7Sara ThomasBengaluru

Join two tables

Each order with its customer’s name:

SQL
SELECT o.order_id, o.order_date, c.customer_name, c.cityFROM orders oINNER JOIN customers c ON c.customer_id = o.customer_idORDER BY o.order_id;
ResultMySQL 8.4
order_idorder_datecustomer_namecity
10012025-01-05Ananya SharmaMumbai
10022025-01-17Rohit KumarDelhi
10032025-02-02Ananya SharmaMumbai
10042025-02-14Fatima KhanHyderabad
10052025-03-08David FernandesGoa
10062025-03-22Ishita BoseKolkata
10072025-04-10Rohit KumarDelhi
10082025-04-28Ananya SharmaMumbai
10092025-05-15Fatima KhanHyderabad
10102025-06-01Ishita BoseKolkata
10 rows in set
How this query works, step by step
  1. FROM orders o starts with the 10 orders; o is the table alias.
  2. For each order, INNER JOIN customers c ON c.customer_id = o.customer_id looks up the customer with the same id.
  3. Every order has a valid customer (the foreign key guarantees it), so all 10 orders come back.
  4. Customers 6 and 7 have no orders, so they never appear: an inner join keeps only matched rows.

How the matching works

Think of it as a nested loop: for every row on the left, find every row on the right where the ON condition is true, and output one combined row per match.

orders rowON c.customer_id = o.customer_idoutput
1001, customer 1matches Ananya Sharma1 row
1004, customer 3matches Fatima Khan1 row
(no order)Manoj Pillai, Sara Thomas never match0 rows

The database actually uses indexes and smarter algorithms, but the result is the same.

Advertisement

One-to-many joins multiply rows

An order can have several lines. Joining orders to order_items gives one row per line, not per order:

SQL
SELECT o.order_id, o.status, oi.product_id, oi.quantityFROM orders oJOIN order_items oi ON oi.order_id = o.order_idWHERE o.order_id IN (1001, 1002, 1005)ORDER BY o.order_id, oi.product_id;
ResultMySQL 8.4
order_idstatusproduct_idquantity
1001Delivered21
1001Delivered65
1002Delivered41
1005Delivered610
1005Delivered74
5 rows in set
How this query works, step by step
  1. Order 1001 has two lines, so it appears twice.
  2. Order 1002 has one line and appears once.
  3. This is expected, but remember it before you SUM order-level columns after such a join: they would be counted once per line.

Join three and four tables

Customer name, product name and line amount for every order line:

SQL
SELECT c.customer_name, o.order_id, p.product_name, oi.quantity,       oi.quantity * p.price AS amountFROM orders oJOIN customers   c  ON c.customer_id = o.customer_idJOIN order_items oi ON oi.order_id   = o.order_idJOIN products    p  ON p.product_id  = oi.product_idWHERE c.customer_id = 1ORDER BY o.order_id, p.product_name;
ResultMySQL 8.4
customer_nameorder_idproduct_namequantityamount
Ananya Sharma1001Mechanical Keyboard13499
Ananya Sharma1001Notebook Pack51245
Ananya Sharma1003Mechanical Keyboard13499
Ananya Sharma1003Wireless Mouse1799
Ananya Sharma1008Desk Lamp22998
Ananya Sharma1008Office Chair217998
6 rows in set
How this query works, step by step
  1. orders joined to customers gives the customer’s name.
  2. Joining order_items expands each order into its lines.
  3. Joining products adds the name and price of each line.
  4. WHERE c.customer_id = 1 keeps Ananya’s lines: 5 lines across 3 orders.
Advertisement

INNER JOIN with GROUP BY

Revenue per customer, highest first:

SQL
SELECT c.customer_name,       COUNT(DISTINCT o.order_id) AS orders,       SUM(oi.quantity * p.price) AS revenueFROM customers cJOIN orders o       ON o.customer_id = c.customer_idJOIN order_items oi ON oi.order_id   = o.order_idJOIN products p     ON p.product_id  = oi.product_idWHERE o.status <> 'Cancelled'GROUP BY c.customer_id, c.customer_nameORDER BY revenue DESC;
ResultMySQL 8.4
customer_nameordersrevenue
Ananya Sharma330038
Rohit Kumar29798
Ishita Bose26642
Fatima Khan13499
David Fernandes13286
5 rows in set
How this query works, step by step
  1. The joins build one row per order line with its customer and price.
  2. WHERE drops the cancelled order.
  3. GROUP BY the customer adds up the line amounts and counts distinct orders.
  4. Customers without orders are not in the result: use a LEFT JOIN to include them with zero.

Conditions in ON vs WHERE

For an inner join, a filter gives the same result in ON or in WHERE:

SQL
SELECT o.order_id, c.customer_nameFROM orders oJOIN customers c ON c.customer_id = o.customer_id AND c.city = 'Delhi';
ResultMySQL 8.4
order_idcustomer_name
1002Rohit Kumar
1007Rohit Kumar
2 rows in set

Keep join conditions in ON and filters in WHERE for readability. For outer joins the placement changes the result; see LEFT JOIN.

USING

When both columns have the same name, USING (column) is a shortcut for ON a.column = b.column, and the shared column appears only once in SELECT *:

MySQL
SELECT order_id, customer_id, customer_nameFROM ordersJOIN customers USING (customer_id)WHERE order_id < 1004;
ResultMySQL 8.4
order_idcustomer_idcustomer_name
10011Ananya Sharma
10031Ananya Sharma
10022Rohit Kumar
3 rows in set

Common mistakes

  • Forgetting the ON condition (or writing FROM a, b with no WHERE) produces a cross join: every row times every row.
  • Joining on the wrong column, for example ON c.customer_id = o.order_id, runs without an error and returns nonsense. Always check the row count.
  • Ambiguous column names: if both tables have dept_id, write e.dept_id or d.dept_id.
Advertisement

Exercise: Products in order 1008

List the product names and quantities in order 1008.

Expected resultMySQL 8.4
product_namequantity
Office Chair2
Desk Lamp2
2 rows in set
Hint 1

Join order_items to products and filter on order_id.

Exercise: Employees and their department location

Show each employee’s first_name, dept_name and location, for employees whose department is in Mumbai.

Expected resultMySQL 8.4
first_namedept_namelocation
VikramSalesMumbai
NehaSalesMumbai
ArjunSalesMumbai
3 rows in set
Hint 1

Join employees to departments and filter on d.location.

Quick quiz

1. A customer has no orders. Does customers INNER JOIN orders return them?

2. Order 1001 has two order lines. How many rows does it produce in orders JOIN order_items?

3. What is JOIN without a keyword in MySQL?

Advertisement

INNER JOIN FAQ

What does INNER JOIN do in SQL?

It returns rows that have matching values in both tables, according to the condition in ON.

How do I join three tables in SQL?

Add one JOIN ... ON ... per extra table: FROM a JOIN b ON b.x = a.x JOIN c ON c.y = b.y.

Why does my join return duplicate rows?

In a one-to-many relationship each parent row appears once for every matching child row. Aggregate, use DISTINCT deliberately, or join to a pre-aggregated subquery.

Is INNER JOIN faster than LEFT JOIN?

Often slightly, because the optimizer has more freedom to reorder tables. Choose the join that gives the correct result first; indexes on the join columns matter far more.