Schema Design· 15 min

SQL Joins

INNER, LEFT, RIGHT, FULL OUTER — Venn diagram

Relational (SQL)Beginner15 min read

Combine rows from two or more tables based on a related column. Different join types control which rows appear.

SQL Joins (Inner, Left, Right, Full, Cross) — diagram
Table ATable BA ∩ BINNERA ∩ B onlyLEFTAll A + match BRIGHTAll B + match AFULLA ∪ BSELECT * FROM A LEFT JOIN B ON A.id = B.a_id

Explanation

INNER JOIN: only matching rows. LEFT JOIN: all rows from left table, NULLs for non-matching right. RIGHT JOIN: mirror of LEFT. FULL OUTER JOIN: all rows from both. CROSS JOIN: Cartesian product. SELF JOIN: table joined with itself (hierarchies).

Example

-- Find employees and their department names
SELECT e.name, d.name AS department FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;
-- All employees, even without a department
SELECT e.name, d.name AS department FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;

Key points

  • →INNER JOIN drops non-matching rows
  • →LEFT JOIN preserves the 'main' table
  • →CROSS JOIN can be accidental — always include ON
  • →JOIN defaults to INNER

Quick interview answers

JOIN vs INNER JOIN?
Nothing — JOIN defaults to INNER.
Can LEFT JOIN produce more rows?
Yes — if a left row matches multiple right rows, it appears multiple times.

DBMS deep dive

SQL Joins (Inner, Left, Right, Full, Cross): deep dive

The fastest way to master SQL joins is to work them out by hand on a tiny dataset and predict the exact rows. This lesson does that for every join type, then covers the ON-versus-WHERE trap and the algorithms engines use to run joins.

The dataset

employees(id, name, dept_id) has 4 rows: (1, Asha, 10), (2, Ravi, 20), (3, Meena, NULL), (4, Karan, 10). departments(id, name) has 3 rows: (10, Eng), (20, Sales), (30, HR). Meena has no department and HR has no employees. Those two edge cases are where the join types differ.

Exact result of each join on e.dept_id = d.id

JoinRows (employee, department)Count
INNERAsha-Eng, Ravi-Sales, Karan-Eng3
LEFTAsha-Eng, Ravi-Sales, Karan-Eng, Meena-NULL4
RIGHTAsha-Eng, Ravi-Sales, Karan-Eng, NULL-HR4
FULL OUTERAsha-Eng, Ravi-Sales, Karan-Eng, Meena-NULL, NULL-HR5
CROSSevery employee with every department, 4 × 312
LEFT anti (WHERE d.id IS NULL)Meena-NULL1

Notice two things. Meena's NULL never matches anything, not even another NULL, because NULL = NULL is unknown, not true. And Eng appears twice in the INNER result because two employees point to it. A join returns one row per matching pair, not one row per left-side row.

The ON vs WHERE trap

With an INNER JOIN, a filter means the same thing in ON or in WHERE. With an outer join it does not:

Same filter, different results
-- A: filter in WHERE runs after the join, so Ravi (Sales) and Meena (NULL) are removed
SELECT e.name, d.name
FROM employees e LEFT JOIN departments d ON e.dept_id = d.id
WHERE d.name = 'Eng';
-- Asha-Eng, Karan-Eng   (2 rows: this is effectively an INNER JOIN)

-- B: filter in ON limits which departments may match, all employees survive
SELECT e.name, d.name
FROM employees e LEFT JOIN departments d
  ON e.dept_id = d.id AND d.name = 'Eng';
-- Asha-Eng, Ravi-NULL, Meena-NULL, Karan-Eng   (4 rows)

-- Anti-join: departments with no employees (returns HR)
SELECT d.name FROM departments d
WHERE NOT EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);

Rule: conditions on the optional (right) table of a LEFT JOIN go in ON, unless you deliberately want to drop the unmatched rows. Also prefer NOT EXISTS over NOT IN for anti-joins. If the subquery returns even one NULL, NOT IN returns no rows at all. Try WHERE d.id NOT IN (SELECT dept_id FROM employees) on this data and you get nothing back, because of Meena.

Self join and semi join

A self join gives the same table two aliases, for example employees e JOIN employees m ON e.manager_id = m.id to list each employee next to their manager. A semi join (WHERE EXISTS (...)) returns each left row at most once, however many matches it has. Use it when you only care whether a match exists, because it avoids the duplicates that a plain JOIN plus DISTINCT would have to remove.

How engines actually execute joins

AlgorithmHow it worksBest when
Nested loopFor each outer row, look up matching inner rows (ideally through an index)Outer side is small and the inner join column is indexed
Hash joinBuild a hash table on the smaller input, then probe it with the larger oneLarge unsorted inputs with an equality condition
Merge joinWalk two inputs sorted on the join key in stepBoth inputs already sorted, such as from indexes, or the output must be ordered

You never pick the algorithm in SQL. The planner does, based on row estimates. When a join is slow, the cause is usually a bad estimate or a missing index on the inner join column. The query optimization lesson shows how to spot that in a plan.

Common mistakes

  • Joining two one-to-many children (orders and payments) to the same parent and then summing, which multiplies the totals. Aggregate each child in a subquery first.
  • Putting right-table filters in WHERE and silently turning a LEFT JOIN into an INNER JOIN.
  • Using NOT IN with a nullable subquery column.
  • Fixing duplicate rows with DISTINCT instead of correcting the join condition.
  • Forgetting that COUNT(*) counts the NULL-extended rows of a LEFT JOIN. Use COUNT(d.id) to count real matches.

Interview questions

›How many rows can an INNER JOIN of tables with m and n rows return?

Anywhere from 0 to m × n. Zero when nothing matches, and m × n when every row matches every row, for example when all join keys share one value.

›How do you emulate FULL OUTER JOIN in MySQL?

MySQL has no FULL OUTER JOIN. Take the LEFT JOIN result and UNION ALL it with the rows of a RIGHT JOIN where the left key IS NULL. That adds only the unmatched right-side rows, so matched rows are not duplicated.

›What is the difference between a semi join and an inner join?

An inner join returns one row per matching pair, so a left row can repeat. A semi join, written as EXISTS or IN, returns each left row at most once and exposes no columns from the right table.

›What does USING do, and what is a NATURAL JOIN?

USING (col) joins on equally named columns and outputs that column once. NATURAL JOIN implicitly joins on every column with the same name. It is risky, because adding a column such as updated_at to both tables silently changes the join.

›Find the second highest salary per department using a join-free approach.

Use a window function: SELECT * FROM (SELECT e.*, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS r FROM employees e) t WHERE r = 2. DENSE_RANK handles ties correctly.

More Database Management Systems

Compare this lesson with the other 8 concepts in the course. Expand any card for its design, trade-offs and diagram.

Category:
Difficulty:
Showing 8 of 8