Noch keine deutsche Übersetzung — englische Version wird gezeigt. English

SQL JOINs Explained with Visual Examples and Code

Master INNER JOIN, LEFT JOIN, RIGHT JOIN, and anti-joins with step-by-step beginner explanations, table diagrams, and syntax breakdowns.

Auf dieser Seite
  1. Why do we need JOINs?
  2. Our sample tables
  3. Table users
  4. Table orders
  5. 1. INNER JOIN — Keep only matching pairs
  6. Syntax breakdown
  7. Resulting output
  8. 2. LEFT JOIN — Keep every row from the left table
  9. Resulting output
  10. 3. Finding missing or orphan records (The Anti-Join)
  11. How it works
  12. 4. Counting with LEFT JOIN: The COUNT(*) trap
  13. Summary comparison matrix

Why do we need JOINs?

In relational databases, data is organized into specialized tables to prevent duplication (a process called normalization).

Instead of storing user names, email addresses, and phone numbers repeatedly inside every single purchase record, we keep two tables:

  1. users table (stores customer accounts).
  2. orders table (stores purchases, referencing the user via a user_id foreign key).

A JOIN allows you to connect and combine rows from two or more tables in a single query based on a related column between them.


Our sample tables

To understand joins intuitively, imagine these two small tables:

Table users

idname
1Alice
2Bob
3Charlie

Table orders

iduser_idamount
101149.00
102115.00
103299.00

(Notice: Alice placed two orders, Bob placed one order, and Charlie has placed zero orders).


1. INNER JOIN — Keep only matching pairs

An INNER JOIN returns rows only when there is a match in both tables. If a row in the left table has no matching row in the right table (or vice versa), it is excluded from the result.

sql
SELECT users.name, orders.id AS order_id, orders.amount
FROM users
INNER JOIN orders ON orders.user_id = users.id;

Syntax breakdown

  • FROM users: Specifies the first (left) table.
  • INNER JOIN orders: Specifies the second (right) table to join.
  • ON orders.user_id = users.id: The join condition defining how rows correspond to each other.
  • Table Aliases: In practice, developers abbreviate table names using aliases for readability (e.g. FROM users u INNER JOIN orders o ON o.user_id = u.id).

Resulting output

nameorder_idamount
Alice10149.00
Alice10215.00
Bob10399.00

(Charlie is omitted because he has no matching rows in the orders table).


2. LEFT JOIN — Keep every row from the left table

A LEFT JOIN (or LEFT OUTER JOIN) preserves every single row from the left table (users), regardless of whether a matching row exists in the right table (orders). Where no match exists, the columns from the right table are filled with NULL.

sql
SELECT u.name, o.id AS order_id, o.amount
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

Resulting output

nameorder_idamount
Alice10149.00
Alice10215.00
Bob10399.00
CharlieNULLNULL

(Charlie is preserved, with NULL indicating no order records exist).


3. Finding missing or orphan records (The Anti-Join)

One of the most powerful real-world uses of LEFT JOIN is finding records that have never interacted or have missing foreign keys:

sql
-- Find all users who have never placed an order
SELECT u.name, u.id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;

How it works

  1. LEFT JOIN attaches orders to users, producing NULL for o.id whenever a user has zero orders.
  2. The WHERE o.id IS NULL filter eliminates all users who have orders, leaving only the users without orders (in our case: Charlie).

4. Counting with LEFT JOIN: The COUNT(*) trap

When calculating aggregates like "number of orders per user":

sql
-- WRONG: COUNT(*) counts the row itself, so Charlie would show 1 order!
SELECT u.name, COUNT(*) AS total_orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;

-- CORRECT: COUNT(o.id) only counts non-NULL order IDs, so Charlie correctly shows 0!
SELECT u.name, COUNT(o.id) AS total_orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;
Tip

Always use COUNT(right_table.id) instead of COUNT(*) when aggregating over a LEFT JOIN to avoid counting NULL rows as 1.


Summary comparison matrix

JOIN TypeLeft Table RowsRight Table RowsUnmatched Columns Become
INNER JOINOnly if matchedOnly if matchedNot returned
LEFT JOINAll rowsOnly if matchedNULL
RIGHT JOINOnly if matchedAll rowsNULL
FULL OUTER JOINAll rowsAll rowsNULL