SQL Joins Explained: INNER, LEFT, RIGHT and FULL JOIN with Examples

SQL Joins Explained: INNER, LEFT, RIGHT and FULL JOIN with Examples

A database splits its data into separate tables, so customers live in one table and orders in another. A JOIN brings them back together in one result. The join type decides what happens to the rows that have no partner on the other side.

This guide explains every join using the same two small tables, so you can compare the results directly. All the queries were run on a real database, and the visualizer lets you change the data yourself.

Open the SQL joins visualizer Pick a join and see which rows are matched, kept or dropped.

The short version

JoinWhat you getRows in our example
INNER JOINOnly rows that match on both sides4
LEFT JOINAll left rows, plus matches from the right5
RIGHT JOINAll right rows, plus matches from the left5
FULL OUTER JOINAll rows from both sides6
CROSS JOINEvery left row paired with every right row20

If you only learn two, learn INNER JOIN and LEFT JOIN. They cover most of the joins you'll write.

The two example tables

Two tables. customers has Aisha, Omar, Lena and Ravi. orders has Keyboard and Monitor for customer 1, Mouse for customer 2, Webcam for customer 3, and Headset with a NULL customer
Ravi has no orders, and the Headset order has no customer.

Create these tables to follow along. This works in MySQL, MariaDB, PostgreSQL and SQLite:

SQL: create the example tables
CREATE TABLE customers (
  id   INT PRIMARY KEY,
  name VARCHAR(50),
  city VARCHAR(50)
);

CREATE TABLE orders (
  id          INT PRIMARY KEY,
  customer_id INT,
  item        VARCHAR(50),
  total       INT
);

INSERT INTO customers VALUES
  (1, 'Aisha', 'Lahore'),
  (2, 'Omar',  'Karachi'),
  (3, 'Lena',  'Berlin'),
  (4, 'Ravi',  'Mumbai');

INSERT INTO orders VALUES
  (101, 1,    'Keyboard', 45),
  (102, 1,    'Monitor',  180),
  (103, 2,    'Mouse',    20),
  (104, 3,    'Webcam',   60),
  (105, NULL, 'Headset',  75);

The two tables link through orders.customer_id, which holds the id of a customer. That column is called a foreign key. Two rows are there on purpose, because they show the difference between the joins:

How to read a JOIN

SQL: the parts of a join
SELECT c.name, o.item, o.total        -- the columns you want
FROM customers AS c                   -- the left table, with a short name
INNER JOIN orders AS o                -- the right table, with a short name
  ON o.customer_id = c.id;            -- how rows match

The table after FROM is the left table, and the one after JOIN is the right table. ON gives the matching rule. The short names c and o are aliases, and they save a lot of typing.

INNER JOIN: only the matches

Venn diagram with only the overlap shaded, the INNER JOIN query, and a 4 row result: Aisha Keyboard, Aisha Monitor, Omar Mouse, Lena Webcam
Ravi and the Headset both have no partner, so both are dropped.

INNER JOIN keeps a row only when both sides match. Writing JOIN alone means the same thing. Use it when you only care about records that have a partner, such as “show every order with its customer's name”.

Aisha appears twice because she has two orders. A join returns one row for each matching pair, so this is correct. Try it yourself.

LEFT JOIN: keep everything on the left

Venn diagram with the whole customers circle shaded, the LEFT JOIN query, and a 5 row result ending with Ravi, NULL, NULL
Every customer is kept. Ravi has no order, so the order columns are NULL.

LEFT JOIN keeps every row from the left table. Where there is no match, the right-hand columns are NULL. This is the second most useful join, and you need it whenever the question says “all”: all customers, including those who have never ordered.

SQL: orders per customer
SELECT c.name, COUNT(o.id) AS order_count
FROM customers AS c
LEFT JOIN orders AS o
  ON o.customer_id = c.id
GROUP BY c.id, c.name;
nameorder_count
Aisha2
Omar1
Lena1
Ravi0

Notice COUNT(o.id). COUNT(*) counts rows, so it would say Ravi has 1, while COUNT(o.id) skips NULL and correctly gives 0.

RIGHT JOIN: keep everything on the right

Venn diagram with the whole orders circle shaded, the RIGHT JOIN query, and a 5 row result ending with NULL, Headset, 75
Every order is kept. The Headset has no customer, so the name is NULL.

RIGHT JOIN is the mirror image, keeping every row from the right table. In practice most developers don't use it, because swapping the tables gives the same rows as a LEFT JOIN, and reading in one direction is easier. These two queries return the same rows:

SQL: the same result both ways
-- with RIGHT JOIN
SELECT c.name, o.item
FROM customers AS c
RIGHT JOIN orders AS o ON o.customer_id = c.id;

-- with LEFT JOIN, tables swapped
SELECT c.name, o.item
FROM orders AS o
LEFT JOIN customers AS c ON o.customer_id = c.id;

FULL OUTER JOIN: keep everything

Venn diagram with both circles fully shaded, the FULL OUTER JOIN query, and a 6 row result including both Ravi with NULLs and NULL with Headset
Unmatched rows from both sides are kept, with NULL on the missing side.

FULL OUTER JOIN keeps all rows from both tables and matches them where it can. It's useful for comparing two lists, such as finding records that exist in only one of two systems.

MySQL and MariaDB don't have FULL OUTER JOIN. Combine a left and a right join with UNION instead, which also removes the duplicates:

SQL: FULL OUTER JOIN in MySQL
SELECT c.name, o.item, o.total
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id

UNION

SELECT c.name, o.item, o.total
FROM customers AS c
RIGHT JOIN orders AS o ON o.customer_id = c.id;

Find what's missing with LEFT JOIN and IS NULL

Venn diagram with only the customers-only part shaded, a LEFT JOIN query with WHERE o.id IS NULL, and a one row result: Ravi
Only the rows with no match are left: customers who never ordered.

This pattern answers a very common question: which rows have no partner? Do a LEFT JOIN, then keep only the rows where the right side is NULL.

It finds customers with no orders, products never sold, or users who never logged in. Check a column that can't normally be NULL, such as the right table's primary key. See it in the visualizer.

CROSS JOIN: every combination

CROSS JOIN pairs every left row with every right row, and it has no ON. Our 4 customers and 5 orders give 4 × 5 = 20 rows.

SQL: every size with every colour
SELECT s.name AS size, c.name AS colour
FROM sizes AS s
CROSS JOIN colours AS c;

It's useful for building combinations, such as all the size and colour options for a product. Be careful with large tables: two tables of 10,000 rows produce 100 million rows.

Joining more than two tables

Just add another JOIN. Each one adds a table to the result so far:

SQL: three tables
SELECT c.name, o.id AS order_id, p.title
FROM customers AS c
INNER JOIN orders AS o      ON o.customer_id = c.id
INNER JOIN order_items AS i ON i.order_id = o.id
INNER JOIN products AS p    ON p.id = i.product_id;

Be careful when mixing join types. If you start with a LEFT JOIN, a later INNER JOIN on that side drops the unmatched rows you wanted to keep. Once you go left, keep going left.

Self join: a table joined to itself

A self join links rows in the same table, for example staff and their managers. Use two aliases so you can treat it as two tables:

SQL: staff and their managers
SELECT e.name AS employee, m.name AS manager
FROM employees AS e
LEFT JOIN employees AS m
  ON e.manager_id = m.id;

It's a LEFT JOIN so the person at the top, who has no manager, stays in the result.

ON vs WHERE: the classic LEFT JOIN trap

This is the join bug that catches almost everyone. Say you want every customer, plus any orders over 100. These two queries look almost the same, but return different results:

SQL: wrong, Ravi and others vanish
SELECT c.name, o.item
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.total > 100;
SQL: right, every customer stays
SELECT c.name, o.item
FROM customers AS c
LEFT JOIN orders AS o
  ON o.customer_id = c.id AND o.total > 100;

The first returns only one row: Aisha and her Monitor. WHERE runs after the join, and NULL > 100 is not true, so every kept row with NULL is thrown away. Your LEFT JOIN has quietly become an INNER JOIN.

The second returns all four customers, because the filter happens during the matching:

nameitem
AishaMonitor
OmarNULL
LenaNULL
RaviNULL

The rule: with a LEFT JOIN, conditions on the right table go in ON, and conditions on the left table go in WHERE.

Common mistakes

SQL: speed up the join
CREATE INDEX idx_orders_customer ON orders (customer_id);

Try it in the visualizer

The SQL joins visualizer showing join buttons, the customers and orders tables coloured by match, and the Venn diagram and query for LEFT JOIN
Each source row shows whether it was matched, kept or dropped.

In the SQL joins visualizer, green rows matched, yellow rows were kept with no match, and crossed-out rows were dropped. Hover over a result row to see which source rows it came from. Try these:

Share your query

Need a second pair of eyes on a query? Learn how to share a SQL query online, or paste it into the free code sharing tool. No account needed.

Frequently asked questions

What is the difference between INNER JOIN and LEFT JOIN?

INNER JOIN returns only rows that have a match in both tables. LEFT JOIN returns every row from the left table, and fills the right side with NULL where there is no match. If you want to keep rows that have no match, use LEFT JOIN.

Is JOIN the same as INNER JOIN?

Yes. Writing JOIN on its own means INNER JOIN. In the same way, LEFT JOIN and LEFT OUTER JOIN mean the same thing. The word OUTER is optional.

Does MySQL support FULL OUTER JOIN?

No. MySQL and MariaDB do not have FULL OUTER JOIN. You get the same result by combining a LEFT JOIN and a RIGHT JOIN with UNION. PostgreSQL, SQL Server, Oracle and SQLite 3.39 or newer support it directly.

What is the difference between ON and WHERE in a join?

ON decides which rows match during the join. WHERE filters the finished result. With a LEFT JOIN, a condition on the right table belongs in ON. In WHERE, it removes the NULL rows and the join behaves like an INNER JOIN.

Why does my join return duplicate rows?

A join returns one row for every matching pair. If a customer has three orders, that customer appears three times. This is normal in a one-to-many relationship. Use GROUP BY or SELECT DISTINCT if you need one row each.

Why is a row missing from my INNER JOIN?

INNER JOIN drops any row with no match on the other side. NULL values never match anything, not even another NULL, so a row with a NULL key is always dropped. Use LEFT JOIN to keep those rows.

Related articles

← All guides