SQL JOINs

TL;DR

JOINs combine rows from two or more tables based on a related column. INNER returns only matches. LEFT returns all from the left table plus matches. RIGHT is the mirror. FULL returns everything from both sides.

Explain Like I'm 12

Imagine you have two lists. One has student names and their class IDs. The other has class IDs and class names. A JOIN is like laying them side by side and connecting each student to their class name using the class ID they share.

INNER JOIN: Only show students who have a matching class. LEFT JOIN: Show ALL students, even if they don't have a class yet (their class column will be blank). RIGHT JOIN: Show ALL classes, even empty ones with no students. FULL JOIN: Show everything from both lists.

The 4 JOIN Types at a Glance

SQL JOIN types: INNER JOIN returns only matching rows, LEFT JOIN returns all from left table, RIGHT JOIN returns all from right table, FULL OUTER JOIN returns everything

Our Sample Data

We'll use these two tiny tables for every example. Get familiar with them — you'll see them throughout this page.

employees

idnamedept_id
1Alice10
2Bob20
3Charlie10
4DianaNULL

departments

iddept_name
10Engineering
20Marketing
30Sales

Notice: Diana has no department (NULL). Sales has no employees. These edge cases are where different JOINs behave differently.

INNER JOIN

Returns only rows that have a match in both tables. No match = not in the result.

A B matched

Only the overlap

SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;

Result

namedept_name
AliceEngineering
BobMarketing
CharlieEngineering

Diana is gone (no dept_id match). Sales is gone (no employees).

LEFT JOIN

Returns all rows from the left table, plus matched rows from the right. Unmatched right columns become NULL.

A B all of A + match

All of left + overlap

SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;

Result

namedept_name
AliceEngineering
BobMarketing
CharlieEngineering
DianaNULL

Diana is back! Her dept_name is NULL because she has no department. Sales is still missing (it's not in the left table).

When to use LEFT JOIN: When you want "all X, even those without a matching Y." Example: all customers, even those who haven't ordered yet.

RIGHT JOIN

Returns all rows from the right table, plus matched rows from the left. The mirror of LEFT JOIN.

A B match + all of B

Overlap + all of right

SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.id;

Result

namedept_name
AliceEngineering
CharlieEngineering
BobMarketing
NULLSales

Sales is back! But Diana is gone. RIGHT JOIN keeps all departments, even empty ones.

In practice: Most developers rewrite RIGHT JOINs as LEFT JOINs by swapping the table order. It's the same result but easier to read left-to-right.

FULL OUTER JOIN

Returns all rows from both tables. Unmatched columns on either side become NULL.

A B everything

All of both sides

SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.id;

Result

namedept_name
AliceEngineering
BobMarketing
CharlieEngineering
DianaNULL
NULLSales

Everyone and everything. Diana (no dept) and Sales (no employees) both appear with NULLs.

Note: MySQL does not support FULL OUTER JOIN directly. You can emulate it with a UNION of LEFT and RIGHT JOINs.

Comparison Table

JOIN Type Left unmatched Matched rows Right unmatched Use when
INNER Dropped Included Dropped You only want rows that exist in both tables
LEFT Included (NULL-padded) Included Dropped You want all from the left, optionally enriched with right
RIGHT Dropped Included Included (NULL-padded) You want all from the right (rare, use LEFT instead)
FULL Included (NULL-padded) Included Included (NULL-padded) You want everything from both sides, no data lost

Common JOIN Patterns

Multi-table JOIN

Chain JOINs to connect three or more tables:

SELECT e.name, d.dept_name, o.city
FROM employees e
JOIN departments d ON e.dept_id = d.id
JOIN offices o ON d.office_id = o.id;

Self JOIN

Join a table to itself. Useful for hierarchies like employee-manager:

SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Find unmatched rows (anti-join)

LEFT JOIN + WHERE NULL is a powerful pattern to find orphaned records:

-- Find employees with no department
SELECT e.name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id
WHERE d.id IS NULL;

Performance Tips

  • Index your JOIN columns. If you JOIN ON e.dept_id = d.id, make sure both columns are indexed.
  • Filter early with WHERE. Reduce rows before the JOIN happens, not after.
  • Avoid SELECT * in JOINs. You're pulling columns from multiple tables — only select what you need.
  • Use EXPLAIN. Run EXPLAIN SELECT ... to see how the database plans your JOIN. Look for "table scan" warnings.

Test Yourself

Q: What does INNER JOIN return?

Only rows that have a match in both tables. Unmatched rows from either side are dropped.

Q: You want all customers, even those with no orders. Which JOIN?

LEFT JOIN (with customers as the left table and orders as the right).

Q: What appears in the result when a LEFT JOIN has no match on the right side?

NULL values for all columns from the right table.

Q: How do you find orphaned rows (rows in the left table with no match in the right)?

LEFT JOIN + WHERE right_table.id IS NULL. This is called an anti-join pattern.

Q: Why should you index JOIN columns?

Without an index, the database must scan every row in the table to find matches. An index lets it jump directly to matching rows, making JOINs dramatically faster.

Interview Questions

Q: What is the difference between INNER JOIN and LEFT JOIN?

INNER JOIN returns only rows with matches in both tables. LEFT JOIN returns all rows from the left table, filling in NULLs for unmatched right-side columns. Use INNER when you only want matched data. Use LEFT when you want "all from the left side, even without a match."

Q: Can you JOIN a table to itself? When would you?

Yes, it's called a self-join. Common use cases: (1) employee-manager hierarchies (JOIN employees m ON e.manager_id = m.id), (2) finding pairs (e.g., flights with matching departure/arrival), (3) comparing rows within the same table. Always use table aliases to distinguish the two instances.

Q: Write a query to find all departments with no employees.

-- Anti-join pattern (most performant)
SELECT d.dept_name
FROM departments d
LEFT JOIN employees e ON d.id = e.dept_id
WHERE e.id IS NULL;

-- Alternative with NOT EXISTS
SELECT dept_name FROM departments d
WHERE NOT EXISTS (
  SELECT 1 FROM employees e WHERE e.dept_id = d.id
);

Q: What's a cross join? When would you use one?

A CROSS JOIN returns every possible combination of rows from both tables (Cartesian product). If table A has 3 rows and table B has 4 rows, you get 12 rows. Use cases: generating all combinations (e.g., all sizes × all colors), creating date scaffolds, or test data generation. Rarely used in production queries.

Q: You have a slow query with multiple JOINs. How do you optimize it?

(1) Run EXPLAIN to see the query plan. (2) Index all JOIN columns. (3) Filter early with WHERE to reduce rows before JOINs. (4) Select only needed columns (no SELECT *). (5) Consider if a subquery or CTE would reduce the intermediate result set. (6) Check for implicit type conversions in JOIN conditions (e.g., joining INT to VARCHAR prevents index usage).