SQL INNER JOIN
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.
- Write an INNER JOIN with
ON - Use table aliases and qualified column names
- Join three and four tables
- Combine joins with
WHERE,GROUP BYandUSING
INNER JOIN syntax
SELECT columnsFROM table1 t1INNER JOIN table2 t2 ON t2.key = t1.key;INNER is optional: JOIN alone means INNER JOIN.
Sample data
| order_id | customer_id | order_date | status |
|---|---|---|---|
| 1001 | 1 | 2025-01-05 | Delivered |
| 1002 | 2 | 2025-01-17 | Delivered |
| 1003 | 1 | 2025-02-02 | Delivered |
| 1004 | 3 | 2025-02-14 | Cancelled |
| 1005 | 4 | 2025-03-08 | Delivered |
| 1006 | 5 | 2025-03-22 | Shipped |
| 1007 | 2 | 2025-04-10 | Delivered |
| 1008 | 1 | 2025-04-28 | Pending |
| 1009 | 3 | 2025-05-15 | Delivered |
| 1010 | 5 | 2025-06-01 | Delivered |
| customer_id | customer_name | city |
|---|---|---|
| 1 | Ananya Sharma | Mumbai |
| 2 | Rohit Kumar | Delhi |
| 3 | Fatima Khan | Hyderabad |
| 4 | David Fernandes | Goa |
| 5 | Ishita Bose | Kolkata |
| 6 | Manoj Pillai | Chennai |
| 7 | Sara Thomas | Bengaluru |
Join two tables
Each order with its customer’s name:
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;| order_id | order_date | customer_name | city |
|---|---|---|---|
| 1001 | 2025-01-05 | Ananya Sharma | Mumbai |
| 1002 | 2025-01-17 | Rohit Kumar | Delhi |
| 1003 | 2025-02-02 | Ananya Sharma | Mumbai |
| 1004 | 2025-02-14 | Fatima Khan | Hyderabad |
| 1005 | 2025-03-08 | David Fernandes | Goa |
| 1006 | 2025-03-22 | Ishita Bose | Kolkata |
| 1007 | 2025-04-10 | Rohit Kumar | Delhi |
| 1008 | 2025-04-28 | Ananya Sharma | Mumbai |
| 1009 | 2025-05-15 | Fatima Khan | Hyderabad |
| 1010 | 2025-06-01 | Ishita Bose | Kolkata |
FROM orders ostarts with the 10 orders;ois the table alias.- For each order,
INNER JOIN customers c ON c.customer_id = o.customer_idlooks up the customer with the same id. - Every order has a valid customer (the foreign key guarantees it), so all 10 orders come back.
- 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 row | ON c.customer_id = o.customer_id | output |
|---|---|---|
| 1001, customer 1 | matches Ananya Sharma | 1 row |
| 1004, customer 3 | matches Fatima Khan | 1 row |
| (no order) | Manoj Pillai, Sara Thomas never match | 0 rows |
The database actually uses indexes and smarter algorithms, but the result is the same.
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:
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;| order_id | status | product_id | quantity |
|---|---|---|---|
| 1001 | Delivered | 2 | 1 |
| 1001 | Delivered | 6 | 5 |
| 1002 | Delivered | 4 | 1 |
| 1005 | Delivered | 6 | 10 |
| 1005 | Delivered | 7 | 4 |
- Order 1001 has two lines, so it appears twice.
- Order 1002 has one line and appears once.
- This is expected, but remember it before you
SUMorder-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:
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;| customer_name | order_id | product_name | quantity | amount |
|---|---|---|---|---|
| Ananya Sharma | 1001 | Mechanical Keyboard | 1 | 3499 |
| Ananya Sharma | 1001 | Notebook Pack | 5 | 1245 |
| Ananya Sharma | 1003 | Mechanical Keyboard | 1 | 3499 |
| Ananya Sharma | 1003 | Wireless Mouse | 1 | 799 |
| Ananya Sharma | 1008 | Desk Lamp | 2 | 2998 |
| Ananya Sharma | 1008 | Office Chair | 2 | 17998 |
ordersjoined tocustomersgives the customer’s name.- Joining
order_itemsexpands each order into its lines. - Joining
productsadds the name and price of each line. WHERE c.customer_id = 1keeps Ananya’s lines: 5 lines across 3 orders.
INNER JOIN with GROUP BY
Revenue per customer, highest first:
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;| customer_name | orders | revenue |
|---|---|---|
| Ananya Sharma | 3 | 30038 |
| Rohit Kumar | 2 | 9798 |
| Ishita Bose | 2 | 6642 |
| Fatima Khan | 1 | 3499 |
| David Fernandes | 1 | 3286 |
- The joins build one row per order line with its customer and price.
WHEREdrops the cancelled order.GROUP BYthe customer adds up the line amounts and counts distinct orders.- 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:
SELECT o.order_id, c.customer_nameFROM orders oJOIN customers c ON c.customer_id = o.customer_id AND c.city = 'Delhi';| order_id | customer_name |
|---|---|
| 1002 | Rohit Kumar |
| 1007 | Rohit Kumar |
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 *:
SELECT order_id, customer_id, customer_nameFROM ordersJOIN customers USING (customer_id)WHERE order_id < 1004;| order_id | customer_id | customer_name |
|---|---|---|
| 1001 | 1 | Ananya Sharma |
| 1003 | 1 | Ananya Sharma |
| 1002 | 2 | Rohit Kumar |
Common mistakes
- Forgetting the ON condition (or writing
FROM a, bwith noWHERE) 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, writee.dept_idord.dept_id.
Exercise: Products in order 1008
List the product names and quantities in order 1008.
| product_name | quantity |
|---|---|
| Office Chair | 2 |
| Desk Lamp | 2 |
Hint 1
Join order_items to products and filter on order_id.
SELECT p.product_name, oi.quantityFROM order_items oiJOIN products p ON p.product_id = oi.product_idWHERE oi.order_id = 1008;| product_name | quantity |
|---|---|
| Office Chair | 2 |
| Desk Lamp | 2 |
Exercise: Employees and their department location
Show each employee’s first_name, dept_name and location, for employees whose department is in Mumbai.
| first_name | dept_name | location |
|---|---|---|
| Vikram | Sales | Mumbai |
| Neha | Sales | Mumbai |
| Arjun | Sales | Mumbai |
Hint 1
Join employees to departments and filter on d.location.
SELECT e.first_name, d.dept_name, d.locationFROM employees eJOIN departments d ON d.dept_id = e.dept_idWHERE d.location = 'Mumbai';| first_name | dept_name | location |
|---|---|---|
| Vikram | Sales | Mumbai |
| Neha | Sales | Mumbai |
| Arjun | Sales | Mumbai |
Quick quiz
1. A customer has no orders. Does customers INNER JOIN orders return them?
INNER JOIN keeps only rows that match.
2. Order 1001 has two order lines. How many rows does it produce in orders JOIN order_items?
One output row per matching line.
3. What is JOIN without a keyword in MySQL?
JOIN defaults to an inner join.
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.