SQL joins simulator

A join pairs rows from two tables using a condition, and the join type decides what happens to rows that match nothing. Edit the two tables below, choose a join, and step through the comparison of every pair of rows. Further down: the result of each join type on the same data, NULLs, ON versus WHERE, and how databases run joins.

Customers
4
Orders
5
Comparisons
20
Result rows
5
Join
Evaluate
Rows
customers c
idnameRemove
orders o
idcustomer_iditemRemove
100
101
102
103
104

Type NULL, or clear a customer_id, to make it NULL.

ON o.customer_id = c.id, for every pair
Customer100
1
101
1
102
2
103
5
104
NULL
Ada 1 true true false false NULL
Grace 2 false false true false NULL
Linus 3 false false false false NULL
Margaret 4 false false false false NULL

true: a match false unknown (a NULL was compared) not checked yet

Query
SELECT c.id, c.name,
       o.id AS order_id, o.customer_id, o.item
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id;
Result
c.idc.nameorder_idcustomer_iditem
1 Ada 100 1 Book
1 Ada 101 1 Lamp
2 Grace 102 2 Pen
3 Linus NULL NULL NULL
4 Margaret NULL NULL NULL

5 rows after 20 of 20 comparisons. SQL does not promise any row order without ORDER BY.

How to use the simulator

The two tables hold customers and their orders. Each order has a customer_id that should point at a customer's id, but not every one does: order 103 points at customer 5, who does not exist, and order 104 has no customer at all (NULL). Customers 3 and 4 have no orders. That mix is what makes the join types give different answers.

Pick a join type and the result appears on the right, with NULL wherever a join filled in a missing side. The grid shows the join condition evaluated for every pair of a customer and an order. Press Step through to watch the evaluation one comparison at a time, in the order a nested loop join would do it. Things to try:

  • Switch between INNER and LEFT. The matching rows are the same; LEFT adds Linus and Margaret with NULLs.
  • Choose FULL OUTER and count: 3 matches, 2 customers without orders, 2 orders without customers.
  • Change order 103's customer_id to 3. Linus gains an order, and one fewer row has NULLs.
  • Set a customer's id to NULL. It can no longer match anything, even an order whose customer_id is also NULL.
  • Step through Semi (EXISTS): once a customer has a match, the remaining orders are skipped.

Every result is computed by evaluating the condition with SQL's three-valued logic. The same data gives the same rows in SQLite 3.45, PostgreSQL and other databases that follow the standard.

What is a SQL join?

A join combines rows from two tables into one result. Conceptually, the database considers every pair made of one row from each table, evaluates the join condition for that pair, and keeps the pairs where the condition is true. In the query on this page the condition is o.customer_id = c.id, so each order is paired with the customer it belongs to.

That is an inner join. The other kinds are called outer joins, and differ only in what they do with rows that matched nothing: a left join keeps the unmatched rows of the first table, a right join those of the second, and a full outer join both, filling the other table's columns with NULL. A cross join has no condition and keeps every pair.

Relational databases split data across tables so that each fact is stored once: a customer's name lives in one row of customers, not in every order. Joins are how queries put the pieces back together.

SQL join examples: each type on the same data

These results come from the simulator's default tables: four customers and five orders. Rows are shown in the order a nested loop produces them; a database may return them in any order unless the query has an ORDER BY.

INNER JOIN: 3 rows

SELECT c.id, c.name,
       o.id AS order_id, o.customer_id, o.item
FROM customers c
INNER JOIN orders o
  ON o.customer_id = c.id;
c.idc.nameorder_idcustomer_iditem
1Ada1001Book
1Ada1011Lamp
2Grace1022Pen

LEFT JOIN: 5 rows

SELECT c.id, c.name,
       o.id AS order_id, o.customer_id, o.item
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id;
c.idc.nameorder_idcustomer_iditem
1Ada1001Book
1Ada1011Lamp
2Grace1022Pen
3LinusNULLNULLNULL
4MargaretNULLNULLNULL

RIGHT JOIN: 5 rows

SELECT c.id, c.name,
       o.id AS order_id, o.customer_id, o.item
FROM customers c
RIGHT JOIN orders o
  ON o.customer_id = c.id;
c.idc.nameorder_idcustomer_iditem
1Ada1001Book
1Ada1011Lamp
2Grace1022Pen
NULLNULL1035Desk
NULLNULL104NULLGift card

FULL OUTER JOIN: 7 rows

SELECT c.id, c.name,
       o.id AS order_id, o.customer_id, o.item
FROM customers c
FULL OUTER JOIN orders o
  ON o.customer_id = c.id;
c.idc.nameorder_idcustomer_iditem
1Ada1001Book
1Ada1011Lamp
2Grace1022Pen
3LinusNULLNULLNULL
4MargaretNULLNULLNULL
NULLNULL1035Desk
NULLNULL104NULLGift card

A CROSS JOIN of the same tables returns 20 rows, one for each of the 4 × 5 pairs, whatever the data.

Inner join vs left join

The difference shows up only when a row of the left table has no match. An inner join drops it; a left join keeps it, with NULLs. Two practical rules follow:

  • Use a left join when every row of the first table must appear, such as "every customer, with their orders if they have any", and an inner join when only rows with a match make sense, such as "orders with the name of the customer who placed them".
  • A left join returns at least as many rows as the left table has. An inner join can return fewer, the same, or more, because of the duplicates described next.

The keywords are interchangeable with longer forms: JOIN means INNER JOIN, and LEFT JOIN means LEFT OUTER JOIN. The word OUTER changes nothing.

Why joins multiply rows

A join produces one row per matching pair, not one row per row of the left table. Ada has two orders, so she appears twice in every join above. This is how a one-to-many relationship should look, but it catches people out in two ways.

The first is aggregates after a join. If customers had a credit column, SUM(c.credit) over the joined rows would count Ada's credit twice. Aggregate the many side first, in a subquery or a common table expression, and join the totals.

The second is joining two one-to-many tables at once. Joining customers to their 3 orders and their 4 support tickets gives 3 × 4 = 12 rows per customer, every order paired with every ticket. Counts and sums over that result are wrong in a way that is easy to miss on small test data.

NULLs in joins

SQL uses three-valued logic: a condition is true, false or unknown. Any comparison involving NULL, including NULL = NULL, is unknown, and a join keeps a pair only when the condition is true. So a row whose join column is NULL never matches anything. Order 104 in the examples has no customer and appears only in the right and full outer joins, as an unmatched row.

Two other kinds of NULL show up in results, and it helps to tell them apart: a NULL that was stored in the table, like order 104's customer_id, and a NULL that the outer join filled in because there was no matching row. To join on columns where NULL should match NULL, use a IS NOT DISTINCT FROM b (PostgreSQL, SQL Server 2022, SQLite's IS, MySQL's <=>), knowing that it usually stops the database from using a plain index lookup.

ON vs WHERE in a left join

For an inner join, a condition gives the same result in ON or in WHERE. For an outer join it does not. ON decides which rows match, and unmatched left rows are kept anyway. WHERE runs afterwards, on the joined rows, and the NULLs that the join filled in fail almost any condition. These two queries differ only in where one condition is written; the results were checked in SQLite 3.45:

-- 5 rows: every customer, with orders other than pens
SELECT c.name, o.id, o.item
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.item <> 'Pen';

-- 2 rows: Ada's book and lamp only
SELECT c.name, o.id, o.item
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.item <> 'Pen';

In the second query, Grace's pen is removed by the WHERE clause, and Linus and Margaret are removed too, because NULL <> 'Pen' is unknown. The left join has turned into an inner join. Conditions on the right table of a left join belong in ON, unless the aim is to test for a missing match with WHERE o.id IS NULL.

Semi-joins and anti-joins

Often the question is not "which orders" but "which customers have any orders", or "which have none". These are a semi-join and an anti-join. They return rows of one table only, each at most once, so they never duplicate rows. SQL has no keyword for them; they are written with EXISTS and NOT EXISTS, and databases run them as joins.

QuestionUsual SQLDefault data
Customers with at least one orderWHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)Ada, Grace
Customers with no ordersWHERE NOT EXISTS (…)Linus, Margaret
Same, with a left joinLEFT JOIN orders o ON … WHERE o.id IS NULLLinus, Margaret
Same, with NOT INWHERE c.id NOT IN (SELECT customer_id FROM orders)No rows at all

The last row is a well-known trap, checked in SQLite 3.45 on the default data. c.id NOT IN (1, 1, 2, 5, NULL) expands to a chain of <> comparisons joined by AND, and the comparison with NULL is unknown, so the condition is never true for any customer. One NULL in the subquery empties the result. Prefer NOT EXISTS, or filter the NULLs out of the subquery.

Self joins, natural joins and USING

  • A self join joins a table to itself under two aliases, for example employees to their managers: FROM employees e JOIN employees m ON m.id = e.manager_id.
  • JOIN … USING (customer_id) is shorthand for an equality on columns with the same name in both tables, and returns that column once.
  • A natural join joins on every column the two tables have in common by name. Adding a column such as created_at to both tables later silently changes its meaning, so most style guides avoid it.
  • A non-equi join uses a condition other than equality, such as matching each sale to the price band it falls in with BETWEEN.
  • LATERAL (PostgreSQL, MySQL 8.0.14 and later) or CROSS APPLY (SQL Server) lets the right side refer to the current left row, for queries like "the three latest orders of each customer".

Why Venn diagrams mislead

Joins are often drawn as two overlapping circles, with the overlap shaded for an inner join. The picture suggests that a join works on sets of identical items, like UNION and INTERSECT do. It does not: a join builds new rows out of pairs, one table's rows need not resemble the other's, and one left row can produce many result rows. The circles cannot show the duplicates of a one-to-many join, the effect of a NULL key, or a cross join at all. Lukas Eder's 2016 post "Say NO to Venn Diagrams When Explaining JOINs" makes the case in detail. The grid in the simulator, with every pair and its outcome, is closer to what the database does.

How databases run joins

Checking every pair, as the simulator does, takes time proportional to the product of the two table sizes, which is fine for small tables and hopeless for large ones. Databases choose among three main algorithms, and the query planner picks one using its estimates of table sizes.

AlgorithmHow it worksGood when
Nested loopFor each row of the outer input, look for matches in the inner input, ideally with an index lookup instead of a scan.The outer input is small and the inner side has an index on the join column.
Hash joinBuild a hash table on the join key of the smaller input, then probe it with each row of the larger one.Large inputs, equality conditions, no useful index.
Merge joinSort both inputs on the join key (or read them already sorted from an index) and walk them in step.Both inputs are already sorted, or the result must be sorted anyway.

Run EXPLAIN (or EXPLAIN ANALYZE in PostgreSQL) to see which algorithm was used and how many rows each step produced. A foreign key column such as orders.customer_id is not indexed automatically in PostgreSQL or SQL Server, and adding that index is one of the most common join speed-ups. MySQL's InnoDB does create one for each foreign key. The query execution simulator walks through plans like these.

Join support across databases

DatabaseRIGHT JOINFULL OUTER JOINNotes
PostgreSQLYesYesLATERAL joins; IS NOT DISTINCT FROM.
MySQLYesNoEmulate FULL with a LEFT JOIN, UNION, and a RIGHT JOIN; <=> compares NULLs as equal.
SQLiteSince 3.39.0 (2022)Since 3.39.0 (2022)Earlier versions supported only inner, left and cross joins.
SQL ServerYesYesCROSS APPLY and OUTER APPLY for lateral joins.
OracleYesYesOlder code uses the (+) operator for outer joins.

SQL joins in interviews

Join questions in interviews usually test one of these points:

  • Row counts. "Table A has 4 rows, table B has 5. How many rows can an inner join return?" Anywhere from 0 to 20, depending on the data; a left join returns at least 4; a cross join exactly 20.
  • Finding missing rows. Customers with no orders: NOT EXISTS, or LEFT JOIN with WHERE o.id IS NULL. Mention the NOT IN and NULL trap.
  • ON vs WHERE in an outer join, as above.
  • Duplicates and aggregates. Why a total is too high after a join, and how to aggregate before joining.
  • Self joins, such as employees earning more than their managers.
  • Performance. Which index helps a join, and what nested loop, hash and merge joins are.

SQL join questions and answers

What is the difference between INNER JOIN and LEFT JOIN?

An INNER JOIN returns only pairs of rows where the join condition is true. A LEFT JOIN returns the same pairs plus every row of the left table that matched nothing, with NULL in the right table’s columns. A LEFT JOIN never returns fewer rows than the left table has.

What is a FULL OUTER JOIN?

A FULL OUTER JOIN returns the matching pairs, plus the unmatched rows from the left table padded with NULLs, plus the unmatched rows from the right table padded with NULLs. It is the combination of a LEFT and a RIGHT join. MySQL does not support it; the usual workaround is a LEFT JOIN combined with a RIGHT JOIN using UNION.

Is RIGHT JOIN the same as LEFT JOIN?

A RIGHT JOIN keeps every row of the right table instead of the left one. Swapping the two tables turns any RIGHT JOIN into a LEFT JOIN with the same rows, which is why many teams write only LEFT JOINs.

What is a CROSS JOIN?

A CROSS JOIN pairs every row of one table with every row of the other and has no join condition. Two tables of 1,000 rows give 1,000,000 rows. It is useful for generating combinations, such as every product in every size.

Why does my join return more rows than the table has?

Because a row that matches several rows in the other table appears once per match. Joining customers to orders gives one row per order, so a customer with three orders appears three times. Summing a column from the customers table after such a join counts each customer once per order.

Do NULL values match in a SQL join?

No. NULL = NULL is unknown, not true, so a row with NULL in the join column never matches anything, not even another NULL. To treat NULLs as equal, use IS NOT DISTINCT FROM where the database supports it, or compare COALESCE(a, sentinel) values.

What is the difference between ON and WHERE in a LEFT JOIN?

Conditions in ON decide which rows match; unmatched left rows are still kept. Conditions in WHERE filter the joined result afterwards, and because unmatched rows have NULL in the right table’s columns, a WHERE condition on those columns removes them, which turns the LEFT JOIN into an INNER JOIN.

Why does NOT IN return no rows when the subquery contains NULL?

x NOT IN (1, 2, NULL) means x <> 1 AND x <> 2 AND x <> NULL. The last comparison is unknown, so the whole condition is never true and no rows are returned. NOT EXISTS does not have this problem, which is why it is the safer way to write an anti-join.

Which join is fastest?

The join type matters less than how the database executes it. Databases choose between nested loop joins (good when one side is small or there is an index on the join column), hash joins (good for large unsorted inputs and equality conditions) and merge joins (good when both inputs are already sorted). EXPLAIN shows which one was used.

Further reading

Published Updated