skip to content

Join Types

The join constructs themselves — inner, outer, cross, self, USING/NATURAL, and LATERAL — and exactly which rows each one keeps, drops, or NULL-extends. Interviewers ask you to predict result-set contents and row counts for each construct, often on tiny sample tables.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

How many rows does a CROSS JOIN of a 100-row table and a 20-row table return, and why?

level: juniorimportance: must knowfreq 78%

answer

  1. no matching condition is involved
  2. every left row meets every right row
  3. the count is a product, not a sum
  4. multiply the two row counts together

basics

~20 s

2,000 rows. A CROSS JOIN returns the Cartesian product: every row of the left table paired with every row of the right, with no join predicate. The output size is N times M, never N plus M.

solid answer

~40 s

A `CROSS JOIN` is the only join with no `ON` condition — it pairs each left row with each right row unconditionally, so 100 rows crossed with 20 rows gives 100 × 20 = 2,000 rows. Because nothing filters the pairing, no row is ever dropped for lack of a match and no column is ever NULL-extended, which is what separates it from inner and outer joins. The result is a product, so it grows multiplicatively: cross three tables of 1,000 rows each and you have a billion rows. Two edge cases follow straight from the arithmetic: if either side is empty the result is empty (N × 0 = 0), and crossing with a one-row table just copies that row's columns onto every row of the other side.

code

sql · 5 lines
sql
-- 3 colors x 2 sizes = 6 rows
SELECT c.color, s.size
FROM colors c
CROSS JOIN sizes s
ORDER BY c.color, s.size;

go deeper

for a junior

Recall the formula N times M and be able to enumerate a small example out loud, such as three colors crossed with two sizes giving six rows. Know that CROSS JOIN takes no ON clause.

for a middle

Explain why the count multiplies rather than adds, what happens when one side is empty or has exactly one row, and that a comma-separated FROM list with no relating predicate is the same operation written implicitly.

for a senior

Be ready to use the arithmetic diagnostically: when a query returns a suspicious row count, check whether it factors into the sizes of two joined tables, and say what predicate is missing.

for a principal

Frame it as a review habit — insist that any intentional Cartesian product is written with the explicit CROSS JOIN keyword so reviewers can distinguish intent from a forgotten join predicate.

## What CROSS JOIN does `CROSS JOIN` is the unconditional join. It takes two row sources and emits one output row for every possible pairing of a row from the left with a row from the right. Unlike `INNER JOIN`, `LEFT JOIN` and the rest, it takes no `ON` clause and no `USING` list, because there is nothing to decide: every pair qualifies. ```sql SELECT c.color, s.size FROM colors c CROSS JOIN sizes s; ``` If `colors` holds `red, green, blue` and `sizes` holds `S, M`, the result is six rows: `(red,S) (red,M) (green,S) (green,M) (blue,S) (blue,M)`. ## The arithmetic The rule is simply `rows(left) × rows(right)`. For a 100-row table crossed with a 20-row table that is 2,000 rows. The mistake interviewers listen for is addition — answering 120 — which betrays a mental model of "stacking" rather than "pairing". Stacking is what `UNION ALL` does; multiplication is what a join does. The multiplication chains. `a CROSS JOIN b CROSS JOIN c` returns `|a| × |b| × |c|`. Three tables of a thousand rows each produce a billion output rows — which is why an accidental cross join is one of the classic ways to make a small database look like a hung server. The operation is commutative in content: `a CROSS JOIN b` and `b CROSS JOIN a` contain the same set of pairings and the same row count, differing only in column order (and in whatever order the engine happens to return rows, which is undefined without `ORDER BY`). ## Edge cases that fall out of the formula - **Empty side.** If either input has zero rows, the product is zero rows. There is no NULL-extension: `CROSS JOIN` never invents a partner the way an outer join does. - **One-row side.** Crossing an N-row table with a one-row table returns exactly N rows, with the single row's columns copied onto each. This is a deliberate and common idiom for attaching a grand total or a parameter row to every result row. - **Duplicates.** `CROSS JOIN` does not deduplicate. If the left table has three identical rows, each of them is paired with every right row, so all three pairings appear. ## Syntax forms The standard spells it `A CROSS JOIN B`. Writing the two tables in a comma-separated `FROM` list — `FROM colors, sizes` — with no predicate relating them yields exactly the same Cartesian product; the explicit keyword is preferred precisely because it announces the intent, so a reviewer can tell a deliberate product from a forgotten predicate. Some engines also accept `A JOIN B ON TRUE` or `A INNER JOIN B ON 1 = 1`, which is the same thing written as an always-true predicate. ```sql -- these three describe the same result SELECT * FROM colors CROSS JOIN sizes; SELECT * FROM colors, sizes; SELECT * FROM colors JOIN sizes ON TRUE; ``` ## Why anyone wants a Cartesian product Deliberate cross joins solve the problem of rows that *should* exist but do not. A sales table contains no row for a product that sold nothing on Tuesday, so a report built only from sales silently omits Tuesday. Crossing a list of days with a list of products manufactures the full grid of `(day, product)` combinations first, and the real data is attached to that grid afterwards. The same shape generates test fixtures (every plan crossed with every currency), pairing matrices, and per-row copies of a single summary row. ## Reasoning about the number in an interview When a query returns far more rows than the base table has, the first arithmetic to run is exactly this one: is the observed count a plausible product of two table sizes? A join returning 20,000,000 rows out of tables of 10,000 and 2,000 rows is not a mystery — it is 10,000 × 2,000, and the missing piece is a join predicate. Being fluent with N × M is what makes that diagnosis instant rather than a debugging session.

  • What does a CROSS JOIN return when one of the two tables is empty?
    Zero rows. The result size is the product of the two row counts, and anything times zero is zero. A `CROSS JOIN` never NULL-extends the surviving side the way an outer join does — there is no notion of an unmatched row, so an empty input simply annihilates the result.
  • Does the order of the two tables change the result of a CROSS JOIN?
    Not in content or cardinality: `a CROSS JOIN b` and `b CROSS JOIN a` produce the same set of pairings and the same row count. Only the column order in `SELECT *` differs. Row order is undefined for either form unless you add `ORDER BY`.
  • How does CROSS JOIN differ from UNION ALL, which also makes a result bigger?
    `UNION ALL` stacks rows vertically: it concatenates two result sets that must have compatible column counts and types, giving N + M rows and the same columns. `CROSS JOIN` widens horizontally: it pairs rows, giving N × M rows with the columns of both tables side by side.

It is a restaurant menu's build-your-own section: three breads and four fillings do not give you seven choices, they give you twelve sandwiches — every bread with every filling.

saying these in an interview costs you the question

  • Answers 120, adding the row counts instead of multiplying
  • Thinks CROSS JOIN needs an ON clause to be valid
  • Claims CROSS JOIN drops rows that have no match
  • Says an empty table still yields the other table's rows
  • Believes CROSS JOIN removes duplicate pairings automatically

context

open as a page

What rows does a FULL OUTER JOIN return that an INNER JOIN of the same tables does not?

level: juniorimportance: must knowfreq 70%

basics

~20 s

FULL OUTER JOIN returns every matched pair plus every unmatched row from both tables. An unmatched left row gets NULLs in all right-side columns, and an unmatched right row gets NULLs in all left-side columns.

open as a page

What does an INNER JOIN ... ON return, and which rows does it silently drop?

level: juniorimportance: must knowfreq 88%

basics

~20 s

INNER JOIN emits one output row for every pair of left and right rows whose ON predicate evaluates to TRUE. Any row on either side with no qualifying partner is dropped from the result, with no warning and no NULL placeholder.

open as a page

What does LEFT OUTER JOIN return for left rows that have no match on the right?

level: juniorimportance: must knowfreq 88%

basics

~10 s

LEFT OUTER JOIN keeps every row of the left table. When the ON predicate matches no right row, the left row is still returned once, with every right-side column filled in as NULL.

open as a page

Why must a self-join give the table two aliases, as in employees e JOIN employees m?

level: juniorimportance: must knowfreq 78%

basics

~20 s

A join combines two row sources, not two files. Aliasing the same table twice creates two independently named sources, so e.manager_id and m.employee_id refer to different rows. Without aliases every column reference is ambiguous and the statement is rejected.

open as a page

How does JOIN ... USING (customer_id) differ from an equivalent JOIN ... ON predicate?

level: juniorimportance: must knowfreq 55%

basics

~20 s

USING applies the same equality test as the matching ON predicate, but merges the two same-named columns into a single output column: SELECT * returns customer_id once, and the rest of the query references it unqualified.

open as a page

Why does SELECT * FROM orders o, customers c WHERE o.total > 100 return millions of rows?

level: middleimportance: must knowfreq 68%

basics

~20 s

The comma in the FROM list relates nothing, so the query is an accidental Cartesian product. The WHERE clause filters only orders, then every surviving order is paired with every customer: the result is (matching orders) times (customers) rows.

open as a page

Why can an INNER JOIN return more rows than either of the joined tables contains?

level: middleimportance: must knowfreq 72%

basics

~20 s

A join emits one row per qualifying pair, not per input row. If a key value appears twice on the left and three times on the right, those rows alone produce six output rows, so the result can far exceed both inputs.

open as a page

What does the LATERAL keyword let a derived table in FROM do?

level: middleimportance: must knowfreq 45%

basics

~20 s

LATERAL lets a derived table in the FROM clause reference columns of the FROM items written before it. Semantically the subquery is evaluated once per left-hand row, so a correlated subquery becomes a join that can return many rows and many columns.

open as a page

With a self-join, how do you find every row in users that shares an email with another row?

level: middleimportance: must knowfreq 65%

basics

~20 s

Join users to itself on equal email and unequal key: ON a.email = b.email AND a.user_id <> b.user_id. Every row that has a twin is returned, once per twin, so use DISTINCT when three or more rows can share an address.

open as a page

How do you list customers with no orders using a LEFT JOIN and IS NULL?

level: juniorimportance: should knowfreq 80%

basics

~20 s

LEFT JOIN orders onto customers, then keep only the rows the join failed to match by testing a never-NULL right-side column in the WHERE clause, for example WHERE o.customer_id IS NULL. Those rows are the customers with no order.

open as a page

Is FROM a CROSS JOIN b WHERE a.id = b.a_id equivalent to FROM a INNER JOIN b ON a.id = b.a_id?

level: middleimportance: should knowfreq 46%

basics

~20 s

Yes for inner joins: the standard defines an inner join as the Cartesian product filtered by the join predicate, so both forms return the same rows. CROSS JOIN accepts no ON clause of its own, and the explicit INNER JOIN form reads better.

open as a page

Why does a FULL OUTER JOIN result need COALESCE(l.customer_id, r.customer_id) for its key column?

level: middleimportance: should knowfreq 52%

basics

~20 s

A full outer join NULL-extends whichever side is unmatched, so each key column is NULL for rows that came only from the other table. COALESCE(l.customer_id, r.customer_id) merges the two into one column that is always populated.

open as a page

How do you emulate a FULL OUTER JOIN on an engine that supports only LEFT and RIGHT joins?

level: middleimportance: should knowfreq 58%

basics

~20 s

Run a LEFT JOIN for the matched and left-only rows, then UNION ALL a second branch that keeps only the right-side rows with no left match. Use UNION ALL, not UNION, so legitimate duplicate rows survive.

open as a page

Is the comma join FROM orders o, customers c WHERE o.customer_id = c.id the same as INNER JOIN ... ON?

level: middleimportance: should knowfreq 58%

basics

~20 s

For an inner join the two forms return exactly the same rows: a comma in FROM is a cross join, and the WHERE predicate then filters it. Explicit JOIN ... ON is preferred because it separates join conditions from filters and can express outer joins.

open as a page

When would you write an INNER JOIN whose ON predicate is a range instead of an equality?

level: middleimportance: should knowfreq 45%

basics

~20 s

When the match is defined by containment rather than equal keys: assigning an amount to a price tier, a timestamp to a validity window, or a value to a bucket. The ON clause accepts any boolean predicate, not only column equality.

open as a page

What does LEFT JOIN LATERAL ... ON TRUE give you that JOIN LATERAL does not?

level: middleimportance: should knowfreq 35%

basics

~20 s

It keeps left-hand rows whose LATERAL subquery returned no rows, NULL-extending the subquery's columns instead of dropping the row. The inner forms, CROSS JOIN LATERAL and JOIN LATERAL ... ON TRUE, discard those rows entirely.

open as a page

Why can every RIGHT OUTER JOIN be rewritten as a LEFT OUTER JOIN?

level: middleimportance: should knowfreq 52%

basics

~20 s

RIGHT OUTER JOIN preserves the table written after the keyword, and LEFT preserves the one written before it. Swapping the two table references and changing RIGHT to LEFT therefore yields the same rows; only the default column order changes.

open as a page

Why does pairing a table with itself using a.id <> b.id return every pair twice?

level: middleimportance: should knowfreq 48%

basics

~20 s

The predicate a.id <> b.id keeps both orderings of every pair, so (1,2) and (2,1) both survive. Replacing it with a.id < b.id keeps exactly one ordering, cutting n(n-1) rows to n(n-1)/2 while still excluding self-pairs.

open as a page

What does COUNT(DISTINCT h.salary) + 1 compute in a self-join with ON h.salary > e.salary?

level: middleimportance: should knowfreq 52%

basics

~20 s

It computes each employee's salary rank: the number of distinct salaries above theirs, plus one for their own. Tied salaries share a rank and the numbering has no gaps, matching dense-rank behaviour, provided the join is a LEFT JOIN so the top earner survives.

open as a page

Which columns does NATURAL JOIN match on, and what does it return if the tables share none?

level: middleimportance: should knowfreq 45%

basics

~20 s

NATURAL JOIN implicitly equates every column name the two tables share and merges each matched pair into one output column. If they share no column names the join condition is empty, so the result is a Cartesian product.

open as a page

In JOIN ... USING (dept_id), how must you reference dept_id, and is d.dept_id portable?

level: middleimportance: should knowfreq 35%

basics

~20 s

The standard makes the USING column a column of the join, referenced unqualified as dept_id. Qualifying it is not portable: Oracle rejects a qualifier on a USING column, while PostgreSQL resolves it to that table's own copy.

open as a page

How do you use CROSS JOIN to report zero-sale days for every product in a date range?

level: seniorimportance: should knowfreq 40%

basics

~20 s

CROSS JOIN a date list to the product list to manufacture every (day, product) pair, LEFT JOIN the sales table on both keys, then COALESCE the aggregate to 0 so pairs with no sale still report a row.

open as a page

How do you reconcile two tables with a FULL OUTER JOIN and label rows missing on either side or mismatched?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Full outer join the two sets on the business key, COALESCE the keys into one column, then use CASE: a NULL key on one side means the row is missing there, and otherwise compare the value columns NULL-safely to flag mismatches.

open as a page

Why does adding an INNER JOIN to a report sometimes make rows disappear?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Because an inner join is also a filter. Attaching a lookup table asserts that a matching row exists, so any fact row whose key is missing, orphaned or unmatched is removed from the result — silently, with no error and no NULL placeholder.

open as a page

How do you use LATERAL to return each customer's three most recent orders?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Drive the query from one row per group and join a LATERAL subquery that selects from the detail table, correlates on the group key, and carries its own ORDER BY with FETCH FIRST 3 ROWS ONLY. The limit then applies per left row, not to the whole result.

open as a page

Why does one LATERAL join beat three scalar correlated subqueries in a SELECT list?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Three scalar subqueries are three independent per-row lookups, so on ties they can return columns from different rows and each must return at most one row. One LATERAL join picks a row once and exposes all of its columns together, consistently.

open as a page

Why does a join chain that starts with LEFT JOIN lose the preserved rows once an INNER JOIN follows?

level: seniorimportance: should knowfreq 46%

basics

~20 s

A join chain is evaluated left to right, so the inner join runs against the already NULL-extended intermediate result. Its predicate compares a NULL column, evaluates to unknown, and the preserved rows are discarded — turning the whole chain inner.

open as a page

Using a self-join, how do you pair each sensor reading with the previous reading for that device?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A join has no notion of "the row before", so the ON clause must define it: join the table to itself on the same device with an earlier timestamp, then keep only the nearest earlier row — via a MAX subquery or a NOT EXISTS check that no reading falls in between.

open as a page

A NATURAL JOIN report returned rows until a migration added created_at to both tables — why, and how do you fix it?

level: seniorimportance: should knowfreq 32%

basics

~20 s

NATURAL JOIN re-derives its condition from whatever column names the two tables currently share, so created_at silently joined the key list and rows now match only when both timestamps are equal — almost never. Replace it with an explicit ON or USING.

open as a page

showing 1–30 of 33