SQLJOININNER JOINtutorial

What is INNER JOIN in SQL? Joining tables for beginners

INNER JOIN combines rows from two tables by a shared key. The simplest and most common JOIN. We cover the syntax, ON clause, multi-table joins, common mistakes, and three practice exercises.

7 min readSQL · JOIN · INNER JOIN · tutorial · beginner

INNER JOIN (or just JOIN) is the SQL command for combining two tables by a shared key. The simplest and most common JOIN type — you can't go far with relational databases without it.

Quick analogy. You have two tables:

  • users — users (id, name)
  • orders — orders (id, user_id, amount)

There's no buyer name in orders — only user_id. To get "buyer name + order amount" pairs, you have to glue these tables back together by a common key — users.id matches orders.user_id. That gluing is INNER JOIN.

Why JOIN matters

A normal database splits data across tables to avoid duplication. The user's name lives only in users; orders references the user via user_id. This is called normalisation.

When you need to show "name + order", you have to assemble the data back together — that's what JOIN is for. It's one of the most important SQL topics — without JOIN you can't write a single real-world query.

Basic syntax

SELECT columns
FROM table_A
INNER JOIN table_B
  ON table_A.key = table_B.key;

The word INNER is optional — JOIN without a prefix means INNER JOIN. Most code says just JOIN.

Example: a shop

users table:

idnameemail
1Annaanna@example.com
2Borisbob@example.com
3Veravera@example.com
4Grishagrisha@example.com

orders table:

iduser_idamountcreated_at
1015002024-03-01
11115002024-03-05
1222002024-03-07
13530002024-03-10

Query — join and show buyer name with amount:

SELECT users.name, orders.amount, orders.created_at
FROM users
INNER JOIN orders
  ON users.id = orders.user_id;

Result:

nameamountcreated_at
Anna5002024-03-01
Anna15002024-03-05
Boris2002024-03-07

What happened:

  • Each row from users was paired with each row from orders where users.id = orders.user_id.
  • Anna has two orders → two rows in the result, name duplicated.
  • Boris has one order → one row.
  • Vera and Grisha aren't there — they have no orders. That's the INNER JOIN trait: only pairs with a match in both tables.
  • Order #13 isn't thereuser_id = 5 doesn't exist in users. Sometimes happens, e.g. user got deleted but the order stayed.

If you also want users without orders, that's LEFT JOIN (separate article).

Aliases — short table names

Writing users.name, orders.amount every time is long. Real-world code uses aliases:

SELECT u.name, o.amount, o.created_at
FROM users u
INNER JOIN orders o
  ON u.id = o.user_id;

users u means "give the users table the alias u". Doesn't change the logic — just shorter. In big queries with three-to-five tables, queries without aliases are unreadable.

Note: aliases become mandatory if both tables have a column with the same name. Otherwise the database can't tell whose id you mean.

Joining three or more tables

JOINs chain: A JOIN B JOIN C. Add a products table:

idnameprice
100Bosch kettle49
101iPhone 15999
102Book "Clean Code"22

And extend orders with product_id:

iduser_idproduct_idamount
10110049
11110222
12210049

Query — who bought what:

SELECT u.name AS buyer, p.name AS product, o.amount
FROM users u
INNER JOIN orders o   ON u.id = o.user_id
INNER JOIN products p ON p.id = o.product_id;

Result:

buyerproductamount
AnnaBosch kettle49
AnnaBook "Clean Code"22
BorisBosch kettle49

How it works:

  1. The database first joins users and orders on user_id.
  2. Then the result is joined with products on product_id.
  3. The output keeps only rows where both joins matched.

These chains can be as long as you need. "Users, their orders, products in those orders, product categories, and stores" — that's five JOINs in a row.

JOIN with WHERE

JOIN usually goes with WHERE — JOIN glues tables, WHERE filters the result. E.g. Anna's orders above 1000:

SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.name = 'Anna'
  AND o.amount > 1000;

Extremely common combo — almost any business query is "join the tables + filter with WHERE + maybe aggregate".

INNER JOIN is intersection

In set-theory terms: INNER JOIN is the intersection of two tables on a key. Only the rows present in both make it.

users       orders
  ┌──┐      ┌──┐
  │  │      │  │
  │ ●│ ←  → │● │  ← pairs: only these end up in the result
  │  │      │  │
  └──┘      └──┘

User with no orders — not in the result. Order with no user (broken FK) — not in the result either.

Bigger example: a library

books:

idtitleauthor_id
1War and Peace10
2Anna Karenina10
3Crime and Punishment11
4The Idiot11
5(orphan — author not in DB)99

authors:

idfull_namecountry
10Leo TolstoyRussia
11Fyodor DostoevskyRussia
12Erich Maria RemarqueGermany

Query — every book with author name and country:

SELECT b.title, a.full_name AS author, a.country
FROM books b
JOIN authors a ON a.id = b.author_id
ORDER BY a.full_name, b.title;

Result:

titleauthorcountry
Anna KareninaLeo TolstoyRussia
War and PeaceLeo TolstoyRussia
Crime and PunishmentFyodor DostoevskyRussia
The IdiotFyodor DostoevskyRussia

The orphan book is gone — author_id = 99 isn't in authors. Remarque is gone — he has zero books in our books. INNER JOIN dropped both.

Common beginner mistakes

1. Forgetting the ON clause. Writing JOIN orders with no ON — most DBs either error out or fall back to a CROSS JOIN (Cartesian product, every row times every row). Either an error or a very slow query.

2. Wrong columns in ON. ON u.id = o.id instead of ON u.id = o.user_id is a classic typo. The query won't crash — it'll return nonsense (user #5 paired with order #5, which is meaningless).

3. Ambiguous column names. If both users and orders have id, and you write SELECT id, name FROM users JOIN orders ON …, you get "column reference "id" is ambiguous". Use users.id or aliases.

4. Duplicate rows in the result. If a user has 5 orders, the result has 5 rows with that user's name repeated. Not a bug — it's how JOIN works. Want unique users? Wrap in SELECT DISTINCT u.name or combine with GROUP BY.

5. Thinking JOIN itself is slow. JOINs are fast when there are indexes on the join columns. That's why FK columns (user_id and similar) are always indexed in production.

6. Old comma-style joins. Technically you can write FROM users, orders WHERE users.id = orders.user_id. Don't. The modern JOIN ... ON ... syntax reads better and clearly separates joining from filtering.

Quick recap

  • INNER JOIN combines two tables by a shared key.
  • Only pairs with a match in both tables make it to the result.
  • The join condition goes after ON.
  • The word INNER is optional; people just write JOIN.
  • You can chain many JOINs (three- and five-table queries are normal).
  • Use aliases (users u) to keep things readable.
  • If you also want users with no orders, that's LEFT JOIN, a different scenario.

Try it yourself

On the users and orders tables above:

  1. List buyer-name and amount pairs, sorted by amount descending.
  2. Find all users who placed an order in March 2024.
  3. Count the total order amount per user (combo JOIN + GROUP BY + SUM).

When JOIN clicks for you, it's a freeing moment in learning SQL. Before — you can read one table. After — real-world data opens up fully.

Practice on real tasks

Solve tasks in the SQL trainer with instant grading and hints.

Open trainer