Skip to content

Intermediate: combining and changing data · JOINs · topic 24 of 62 · 30 min

Self joins and cross joins

Explanation

A self join joins a table to itself, using two aliases, which is how you put an employee next to their manager when both live in the employees table. A cross join pairs every row of one table with every row of another, which is useful for building all combinations, such as sizes × colours or dates × stores.

Why join a table to itself?

In COMPANY, manager_id points to another row of the same employees table. Rahul's manager_id is 1, which is Meera's id. To show "Rahul Sharma — Meera Iyer" you need two copies of employees: one playing the employee, one playing the manager. SQL does not copy anything; you simply name the table twice with two different aliases.

Think of it like this: It is like a class photo where every student points to their best friend. To write "Asha → Ravi" you look at the photo once to find Asha, and a second time to find whoever she points to. Same photo, two looks.
sql
SELECT e.name AS employee, m.name AS manager
FROM employees AS e
LEFT JOIN employees AS m ON m.id = e.manager_id;
-- Meera Iyer   | NULL         <- the boss has no manager
-- Rahul Sharma | Meera Iyer
-- Anita Desai  | Rahul Sharma
-- ... 11 rows
e is the employee copy, m the manager copy. LEFT JOIN keeps Meera, whose manager_id is NULL.

The aliases are required here. Without them, employees.name would be ambiguous: which copy do you mean? Choose aliases that say the role, like e and m, or emp and mgr.

Comparing a row with its partner

Once both copies are side by side you can compare them. A classic interview question is "find employees who earn more than their manager".

sql
SELECT e.name, e.salary, m.name AS manager, m.salary AS manager_salary
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
WHERE e.salary > m.salary;
-- (no rows)
In COMPANY every manager earns more than their team, so the correct answer is an empty result.

An empty result is not always a bug. Check the data before "fixing" a query. Here a quick look at the pairs shows the smallest gap is 25000, for Anita and Vikram under Rahul. A more useful question for this data is the size of that gap:

sql
SELECT e.name AS employee, m.name AS manager, m.salary - e.salary AS gap
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
ORDER BY gap;
-- Anita Desai  | Rahul Sharma | 25000
-- Vikram Singh | Rahul Sharma | 25000
-- Neha Joshi   | Farhan Ali   | 37000
-- ...

Pairs within a group

Another self join finds pairs, for example two employees in the same department who could cover for each other. Join on the same dept_id, and add a.id < b.id. Without it, you would get each person paired with themselves (Meera–Meera) and every pair twice (Meera–Rahul and Rahul–Meera).

sql
SELECT a.name, b.name, a.dept_id
FROM employees AS a
JOIN employees AS b ON a.dept_id = b.dept_id AND a.id < b.id;
-- Meera Iyer | Rahul Sharma | 1
-- ...
-- Farhan Ali | Neha Joshi   | 3
-- 10 pairs
Engineering has 4 people, so 4 × 3 / 2 = 6 pairs; Sales 3; Finance 1; HR has only Lakshmi, so none.

CROSS JOIN: every combination

A cross join has no ON. It pairs every row of the first table with every row of the second, so the result has rows(a) × rows(b) rows. This is also called the Cartesian product. Usually it is an accident (a forgotten ON), but sometimes all combinations are exactly what you want.

sql
SELECT s.size, c.colour
FROM sizes AS s
CROSS JOIN colours AS c;
-- S | White
-- S | Indigo
-- M | White
-- ... 3 sizes × 2 colours = 6 variants
A clothing seller creates one product variant (SKU) for every size and colour.

Another common use is a calendar grid: every date × every store, then LEFT JOIN the sales onto it. Days with no sales then show as 0 instead of being missing, which matters for charts. Module 6 shows how to generate the dates.

Careful: Cross joins grow fast. 1,000 customers × 10,000 products is 1 crore rows. SQLite and MySQL accept JOIN without ON and quietly treat it as a cross join; PostgreSQL rejects it with a syntax error. Write CROSS JOIN when you mean it, so readers know it is deliberate.

Common mistakes

  • Using an inner self join for the employee–manager list and losing the top boss. Use LEFT JOIN.
  • Getting each pair twice and people paired with themselves. Add a.id < b.id.
  • Mixing up the roles: ON m.id = e.manager_id means "m is e's manager". Writing ON e.id = m.manager_id swaps them.
  • An accidental cross join from a missing ON, giving a huge result. Check the row count.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Self joins and cross joins · 1/7

Self joins and cross joins

Sometimes a table points to itself. Each employee row has a manager id that points to another employee.

Prefer a video by a teacher? Find free videos on “sql self join employee manager cross join cartesian product” on YouTube ↗

In real life: An org chart in an HR system

HR software stores every employee in one table with a manager_id column. The "reports to" field on a profile page and the approval route for leave requests both come from a self join: find my row, then find the row my manager_id points to. Product catalogues use cross joins to create variants, such as every size of a kurta in every colour.

sql
SELECT e.name AS employee, m.name AS approver
FROM employees AS e
JOIN employees AS m ON m.id = e.manager_id
WHERE e.name = 'Neha Joshi';
Neha Joshi's leave goes to Farhan Ali.

Practice (0/4 done)

Task 1 · Code · SQL

List every employee with the name of their manager. Keep Meera Iyer, who has no manager (her manager should be NULL). Return employee and manager.

Task 2 · Code · SQL

Find every pair of employees who work in the same department, each pair once and nobody paired with themselves. Return the first name (from the copy with the smaller id), the second name, and the dept_id.

Task 3 · Code · SQL

Nobody in COMPANY earns more than their manager. Instead, show how far below their manager each employee is. For every employee who has a manager, return employee, manager and gap (manager salary minus employee salary), smallest gap first, and for equal gaps by employee name A to Z.

Task 4 · Code · SQL

A kurta comes in the sizes and colours stored in the tables sizes(size) and colours(colour). Return every size and colour combination: two columns, size and colour.

Quick quiz

Question 1

1.Why must a self join use two different aliases?

Question 2

2.Pairs of employees in the same department: what does adding a.id < b.id do?

Question 3

3.A table of 5 stores is cross joined with a table of 30 dates. How many rows?

Question 4

4.The query "employees who earn more than their manager" returns no rows on COMPANY. What should you do first?

Question 5

5.In FROM employees e JOIN employees m ON m.id = e.manager_id, which copy is the manager?

Read more (free)

Before this, learn

This topic builds on these. If something here feels new, open them first.

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why must a self join use two different aliases?

Answer: SQL forbids a table name twice without them, and you need to say which copy each column comes from

e.name and m.name tell SQL which role each column belongs to.

Pairs of employees in the same department: what does adding a.id < b.id do?

Answer: Removes self pairs and the mirror copy of each pair

Only one ordering of each pair passes the test, and a.id < a.id is never true.

A table of 5 stores is cross joined with a table of 30 dates. How many rows?

Answer: 150

Every store with every date: 5 × 30.

The query "employees who earn more than their manager" returns no rows on COMPANY. What should you do first?

Answer: Look at the data: if every manager earns more, an empty result is correct

An empty result can be the right answer. Check before changing the query.

In FROM employees e JOIN employees m ON m.id = e.manager_id, which copy is the manager?

Answer: m

m.id equals e's manager_id, so m is the row e points to: the manager.

Employees has 11 rows; one has dept_id NULL. How many rows does employees INNER JOIN departments ON d.id = e.dept_id return (every other employee has a valid department)?Related

Answer: 10

The NULL key matches nothing, so that employee is dropped.

From Why joins, and INNER JOIN

A query joining 1,000 customers and 10,000 orders returns 1,00,00,000 rows. What most likely went wrong?Related

Answer: The ON condition is missing or wrong

Without a correct ON, every customer is paired with every order: 1,000 x 10,000 rows.

From Joins: combining tables

After customers LEFT JOIN orders ... GROUP BY customer, which gives 0 for a customer with no orders?Related

Answer: COUNT(o.id)

COUNT(column) skips NULLs; the unmatched row has o.id NULL.

From LEFT, RIGHT and FULL OUTER JOIN

Which join answers "employees who earn more than their manager"?Related

Answer: A self join: employees e JOIN employees m ON m.id = e.manager_id

Both people are in the same table, so you join the table to itself with two aliases.

From Classic interview queries, part 1

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “JOINs” before its exam: Join pitfalls and how to avoid them.
  2. Module 5 exam: JOINs · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Subqueries, CTEs and set operations. 62 topics left in the course before the final exam.

← Previous: Joining three or more tables