AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

07 Joins / Lesson 2 of 6

LEFT and RIGHT JOIN — keeping the rows without a match

16 min · +10 XP · 2 exercises · 4 questions

Read the text16 min
Exercises0/2
Quiz4 q

Quick answer

What is the difference between LEFT JOIN and INNER JOIN?

An INNER JOIN returns only rows that found a match in both tables. A LEFT JOIN (LEFT OUTER JOIN) returns every row of the left table, matched or not; where the right table has no matching row, its columns come back as NULL. Use a LEFT JOIN whenever rows without a partner, such as customers with no orders, must still appear.

An inner join answers "which pairs match?". Many real questions are different: every customer and how much each spent, including the ones who spent nothing; every product and its last sale, if any; every student and their grade, blank if not yet graded. Dropping the unmatched rows would give a wrong report — the customers who never ordered are often exactly the ones the business wants to see. Outer joins keep them. This lesson covers LEFT JOIN, its mirror RIGHT JOIN, counting and summing after an outer join, and the most common outer-join bug in interviews and production code: a condition in the wrong clause.

LEFT JOIN

SELECT c.name, o.id AS order_id, o.amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.name, o.id;

With the module's data (Asha has orders 10 and 11, Ben has 12, Chen has none, order 13 belongs to a missing customer 9):

LEFT JOIN: every customer, matched or not
customers c LEFT JOIN orders o ON o.customer_id = c.id

LEFT JOIN guarantees every row of the table written first, customers. Each customer is paired with its matching orders, exactly as in an inner join.

0:01 / 0:14Step 1 of 5
Transcript · 5 steps

The rule: every row of the left table appears at least once. A left row with matches appears once per match, exactly as in an inner join. A left row with no match appears once, with every column of the right table set to NULL. Order 13 is still gone — it is on the right side, and the right side keeps only matched rows.

"Left" simply means the table written before LEFT JOIN. Choosing it is choosing whose rows are guaranteed: here, customers.

RIGHT JOIN is the same thing backwards

SELECT c.name, o.id AS order_id
FROM customers c
RIGHT JOIN orders o ON o.customer_id = c.id
ORDER BY o.id;
nameorder_id
Asha10
Asha11
Ben12
NULL13

Now every order is kept and the orphan order 13 shows a NULL name. A RIGHT JOIN B equals B LEFT JOIN A, so it never adds expressive power. Most style guides write only left joins — the preserved table is then always the one in FROM, which you can see at a glance. You should still read right joins fluently, because they appear in legacy code and interview questions.

Counting after an outer join

SELECT c.name,
       COUNT(*)    AS wrong_count,
       COUNT(o.id) AS orders,
       COALESCE(SUM(o.amount), 0) AS spent
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.name;
namewrong_countordersspent
Asha22350
Ben11400
Chen100

Chen's group has one row — the NULL-filled one — so COUNT(*) says 1. COUNT(o.id) counts non-NULL values and says 0, which is the truth. Count a right-side column that is never NULL in a real match (its primary key is ideal). SUM over nothing but NULLs is NULL, not 0; COALESCE(SUM(o.amount), 0) turns it into a number for the report.

The trap: ON versus WHERE

Suppose the report should count only orders from February 2024, but still list every customer.

-- Wrong: the filter is in WHERE
SELECT c.name, COUNT(o.id) AS feb_orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.order_date >= '2024-02-01' AND o.order_date < '2024-03-01'
GROUP BY c.id, c.name
ORDER BY c.name;
namefeb_orders
Asha1
Ben1

Chen vanished. The join produced Chen's NULL-filled row as promised — and then WHERE evaluated NULL >= '2024-02-01', which is unknown, and discarded it. Any WHERE condition on a right-table column that rejects NULL turns the left join back into an inner join.

-- Right: the filter is part of the match
SELECT c.name, COUNT(o.id) AS feb_orders
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id
 AND o.order_date >= '2024-02-01' AND o.order_date < '2024-03-01'
GROUP BY c.id, c.name
ORDER BY c.name;
namefeb_orders
Asha1
Ben1
Chen0
The trap: a right-table filter in WHERE undoes the left join
count February orders, but list every customer

The left join runs first and keeps Chen as promised, with NULL for every order column, order_date included.

0:01 / 0:16Step 1 of 5
Transcript · 5 steps

The rule to memorise:

  • a condition on the right (optional) table that decides which rows count as a match belongs in ON;
  • a condition on the left (preserved) table that decides which left rows appear at all belongs in WHERE — putting it in ON does not filter the left table, it only stops those left rows from matching, and they still appear with NULLs.
-- Only Pune customers, each with their orders (or NULLs)
SELECT c.name, o.id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE c.city = 'Pune'
ORDER BY c.name, o.id;

There is one deliberate exception: WHERE o.id IS NULL after a left join keeps only the unmatched rows. That is the anti-join pattern, covered in the semi- and anti-joins lesson.

Chaining outer joins

Left joins chain left to right: each one keeps every row produced so far.

SELECT c.name, o.id AS order_id, s.shipped_on
FROM customers c
LEFT JOIN orders o    ON o.customer_id = c.id
LEFT JOIN shipments s ON s.order_id = o.id
ORDER BY c.name, o.id;

Chen still appears (NULL order, NULL shipment), and an order with no shipment appears with a NULL date. If the second join were an inner JOIN, its condition s.order_id = o.id would fail for Chen's NULL order, and Chen would disappear — one inner join after a left join quietly undoes it. The joining-many-tables lesson works through that case.

Telling a real NULL from a missing match

If a right-table column can itself be NULL — a shipped_on that is NULL for an order not yet shipped — then a NULL in the result is ambiguous: no matching row, or a matching row with an empty value? Test the right table's primary key instead: s.id IS NULL means no match, because a real row always has its key.

In other databases

LEFT [OUTER] JOIN and RIGHT [OUTER] JOIN are standard and behave identically in PostgreSQL, SQL Server, Oracle and SQLite. Oracle's legacy (+) syntax (WHERE c.id = o.customer_id(+)) is the same left join in old code. The ON-versus-WHERE behaviour is part of the standard, so the trap exists everywhere.

Pitfalls

  • Filtering the optional table in WHERE and losing the unmatched rows.
  • COUNT(*) after a left join counting the NULL row as 1.
  • SUM returning NULL for "nothing" instead of 0.
  • An inner join chained after a left join.
  • Testing a nullable right-side column to detect "no match".

Key takeaways

  • LEFT JOIN keeps every left row; unmatched rows get NULLs for the right table's columns.
  • A RIGHT JOIN B = B LEFT JOIN A; prefer writing left joins.
  • Count a right-side key (COUNT(o.id)), and COALESCE sums to 0.
  • Conditions on the optional table go in ON; conditions on the preserved table go in WHERE.
  • Detect "no match" with the right table's primary key being NULL.

Common questions

Why does my LEFT JOIN behave like an INNER JOIN?

A condition on the right table was put in WHERE. For unmatched rows the right table's columns are NULL, so WHERE o.status = 'paid' is unknown and removes them, undoing the outer join. Move the condition into the ON clause, as in LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid'.

Should I use COUNT(*) or COUNT(column) after a LEFT JOIN?

Count a column of the right table, such as COUNT(o.id). An unmatched left row still produces one result row, so COUNT(*) counts it as 1. COUNT(o.id) skips NULLs and correctly returns 0 for a customer with no orders.

Is RIGHT JOIN ever necessary?

No. A RIGHT JOIN B returns exactly the rows of B LEFT JOIN A, so any right join can be rewritten by swapping the tables. Most teams write only left joins so that the table whose rows are all kept is always the first one named, which makes queries easier to read.

Is LEFT OUTER JOIN the same as LEFT JOIN?

Yes. OUTER is optional in MySQL, PostgreSQL and SQL Server, so LEFT JOIN and LEFT OUTER JOIN are identical, as are RIGHT JOIN and RIGHT OUTER JOIN. The word only signals that unmatched rows are kept.

Mark as readSign in to keep your place.

SQL LEFT JOIN vs RIGHT JOIN: Outer Joins and ON vs WHERE

A LEFT JOIN keeps every row of the left table and fills NULLs where no match exists. RIGHT JOIN mirrors it; filtering in ON vs WHERE changes the answer.

  • Course: SQL study plan
  • Module: Joins
  • Kind: Lesson
  • Reading time: 16 min
  • Runtime: MySQL 8 syntax on SQLite 3.49

What is the difference between LEFT JOIN and INNER JOIN?

An INNER JOIN returns only rows that found a match in both tables. A LEFT JOIN (LEFT OUTER JOIN) returns every row of the left table, matched or not; where the right table has no matching row, its columns come back as NULL. Use a LEFT JOIN whenever rows without a partner, such as customers with no orders, must still appear.

Lesson

An inner join answers "which pairs match?". Many real questions are different: every customer and how much each spent, including the ones who spent nothing; every product and its last sale, if any; every student and their grade, blank if not yet graded. Dropping the unmatched rows would give a wrong report — the customers who never ordered are often exactly the ones the business wants to see. Outer joins keep them. This lesson covers LEFT JOIN, its mirror RIGHT JOIN, counting and summing after an outer join, and the most common outer-join bug in interviews and production code: a condition in the wrong clause.

LEFT JOIN

SELECT c.name, o.id AS order_id, o.amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.name, o.id;

With the module's data (Asha has orders 10 and 11, Ben has 12, Chen has none, order 13 belongs to a missing customer 9):

customers cidname1Asha2Ben3Chenorders oidcustomer_idamount10125011110012240013975no matchresult so farnameorder_idamountAsha10250Asha11100Ben12400ChenNULLNULLkept, padded with NULL
LEFT JOIN: every customer, matched or not. Example: customers c LEFT JOIN orders o ON o.customer_id = c.id
  1. LEFT JOIN guarantees every row of the table written first, customers. Each customer is paired with its matching orders, exactly as in an inner join.
  2. Asha (id 1) matches orders 10 and 11: 2 result rows, one per match.
  3. Ben (id 2) matches order 12: 1 result row, one per match.
  4. Chen matches no order. An inner join would drop Chen; a left join keeps one row with NULL in every column that comes from orders.
  5. Order 13, for the missing customer 9, is still absent: the right side keeps only matched rows. Swap the tables with RIGHT JOIN and it would be the one kept with a NULL name.

The rule: every row of the left table appears at least once. A left row with matches appears once per match, exactly as in an inner join. A left row with no match appears once, with every column of the right table set to NULL. Order 13 is still gone — it is on the right side, and the right side keeps only matched rows.

"Left" simply means the table written before LEFT JOIN. Choosing it is choosing whose rows are guaranteed: here, customers.

RIGHT JOIN is the same thing backwards

SELECT c.name, o.id AS order_id
FROM customers c
RIGHT JOIN orders o ON o.customer_id = c.id
ORDER BY o.id;
nameorder_id
Asha10
Asha11
Ben12
NULL13

Now every order is kept and the orphan order 13 shows a NULL name. A RIGHT JOIN B equals B LEFT JOIN A, so it never adds expressive power. Most style guides write only left joins — the preserved table is then always the one in FROM, which you can see at a glance. You should still read right joins fluently, because they appear in legacy code and interview questions.

Counting after an outer join

SELECT c.name,
       COUNT(*)    AS wrong_count,
       COUNT(o.id) AS orders,
       COALESCE(SUM(o.amount), 0) AS spent
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.name;
namewrong_countordersspent
Asha22350
Ben11400
Chen100

Chen's group has one row — the NULL-filled one — so COUNT(*) says 1. COUNT(o.id) counts non-NULL values and says 0, which is the truth. Count a right-side column that is never NULL in a real match (its primary key is ideal). SUM over nothing but NULLs is NULL, not 0; COALESCE(SUM(o.amount), 0) turns it into a number for the report.

The trap: ON versus WHERE

Suppose the report should count only orders from February 2024, but still list every customer.

-- Wrong: the filter is in WHERE
SELECT c.name, COUNT(o.id) AS feb_orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.order_date >= '2024-02-01' AND o.order_date < '2024-03-01'
GROUP BY c.id, c.name
ORDER BY c.name;
namefeb_orders
Asha1
Ben1

Chen vanished. The join produced Chen's NULL-filled row as promised — and then WHERE evaluated NULL >= '2024-02-01', which is unknown, and discarded it. Any WHERE condition on a right-table column that rejects NULL turns the left join back into an inner join.

-- Right: the filter is part of the match
SELECT c.name, COUNT(o.id) AS feb_orders
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id
 AND o.order_date >= '2024-02-01' AND o.order_date < '2024-03-01'
GROUP BY c.id, c.name
ORDER BY c.name;
namefeb_orders
Asha1
Ben1
Chen0
LEFT JOIN … ON id AND datenameorder_idorder_dateAsha112024-02-11Ben122024-02-20ChenNULLNULLGROUP BY → resultnamefeb_ordersAsha1Ben1Chen0
The trap: a right-table filter in WHERE undoes the left join. Example: count February orders, but list every customer
  1. The left join runs first and keeps Chen as promised, with NULL for every order column, order_date included.
  2. WHERE then tests each joined row. January's order is FALSE; Chen's NULL date makes NULL >= '2024-02-01' UNKNOWN, and WHERE keeps only TRUE.
  3. Only the two TRUE rows reach GROUP BY, so Chen has vanished — the WHERE condition quietly turned the left join into an inner join.
  4. Move the date test into ON and it only decides which orders count as a match. January's order no longer matches Asha, and Chen, matching nothing, is kept with NULLs.
  5. Counting o.id now gives every customer a row: Asha 1, Ben 1, Chen 0. Right-table filters go in ON; left-table filters go in WHERE.

The rule to memorise:

  • a condition on the right (optional) table that decides which rows count as a match belongs in ON;
  • a condition on the left (preserved) table that decides which left rows appear at all belongs in WHERE — putting it in ON does not filter the left table, it only stops those left rows from matching, and they still appear with NULLs.
-- Only Pune customers, each with their orders (or NULLs)
SELECT c.name, o.id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE c.city = 'Pune'
ORDER BY c.name, o.id;

There is one deliberate exception: WHERE o.id IS NULL after a left join keeps only the unmatched rows. That is the anti-join pattern, covered in the semi- and anti-joins lesson.

Chaining outer joins

Left joins chain left to right: each one keeps every row produced so far.

SELECT c.name, o.id AS order_id, s.shipped_on
FROM customers c
LEFT JOIN orders o    ON o.customer_id = c.id
LEFT JOIN shipments s ON s.order_id = o.id
ORDER BY c.name, o.id;

Chen still appears (NULL order, NULL shipment), and an order with no shipment appears with a NULL date. If the second join were an inner JOIN, its condition s.order_id = o.id would fail for Chen's NULL order, and Chen would disappear — one inner join after a left join quietly undoes it. The joining-many-tables lesson works through that case.

Telling a real NULL from a missing match

If a right-table column can itself be NULL — a shipped_on that is NULL for an order not yet shipped — then a NULL in the result is ambiguous: no matching row, or a matching row with an empty value? Test the right table's primary key instead: s.id IS NULL means no match, because a real row always has its key.

In other databases

LEFT [OUTER] JOIN and RIGHT [OUTER] JOIN are standard and behave identically in PostgreSQL, SQL Server, Oracle and SQLite. Oracle's legacy (+) syntax (WHERE c.id = o.customer_id(+)) is the same left join in old code. The ON-versus-WHERE behaviour is part of the standard, so the trap exists everywhere.

Pitfalls

  • Filtering the optional table in WHERE and losing the unmatched rows.
  • COUNT(*) after a left join counting the NULL row as 1.
  • SUM returning NULL for "nothing" instead of 0.
  • An inner join chained after a left join.
  • Testing a nullable right-side column to detect "no match".

Key takeaways

  • LEFT JOIN keeps every left row; unmatched rows get NULLs for the right table's columns.
  • A RIGHT JOIN B = B LEFT JOIN A; prefer writing left joins.
  • Count a right-side key (COUNT(o.id)), and COALESCE sums to 0.
  • Conditions on the optional table go in ON; conditions on the preserved table go in WHERE.
  • Detect "no match" with the right table's primary key being NULL.

Common questions

Why does my LEFT JOIN behave like an INNER JOIN?

A condition on the right table was put in WHERE. For unmatched rows the right table's columns are NULL, so WHERE o.status = 'paid' is unknown and removes them, undoing the outer join. Move the condition into the ON clause, as in LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid'.

Should I use COUNT(*) or COUNT(column) after a LEFT JOIN?

Count a column of the right table, such as COUNT(o.id). An unmatched left row still produces one result row, so COUNT(*) counts it as 1. COUNT(o.id) skips NULLs and correctly returns 0 for a customer with no orders.

Is RIGHT JOIN ever necessary?

No. A RIGHT JOIN B returns exactly the rows of B LEFT JOIN A, so any right join can be rewritten by swapping the tables. Most teams write only left joins so that the table whose rows are all kept is always the first one named, which makes queries easier to read.

Is LEFT OUTER JOIN the same as LEFT JOIN?

Yes. OUTER is optional in MySQL, PostgreSQL and SQL Server, so LEFT JOIN and LEFT OUTER JOIN are identical, as are RIGHT JOIN and RIGHT OUTER JOIN. The word only signals that unmatched rows are kept.

Exercises

Every customer's orders and spend

List every customer with the number of orders they placed and the total they spent. Customers with no orders show 0 and 0. An order whose amount is NULL still counts as an order but adds nothing to the total.

Tables: customers(id, name, city) and orders(id, customer_id, order_date, amount).

customers.idnamecity
1AshaPune
2BenDelhi
3ChenPune
orders.idcustomer_idorder_dateamount
1012024-01-05250
1112024-02-11100
1222024-02-20400
1392024-03-0175

Output: columns customer, orders, spent, ordered by spent descending, then customer ascending.

customer | orders | spent
Ben | 1 | 400
Asha | 2 | 350
Chen | 0 | 0

This term's enrolments for every student

List every student with the number of courses they are enrolled in for the term '2024-S1'. Students with no enrolment in that term show 0. Two students can share a name; they are different students.

Tables: students(id, name) and enrollments(student_id, course_id, term).

students.idname
1Anu
2Bilal
3Chitra
student_idcourse_idterm
11012024-S1
11022024-S1
21012023-S2
31032024-S1
11032023-S2

Output: columns student, courses, ordered by student, then courses.

student | courses
Anu | 2
Bilal | 0
Chitra | 1

In this module: Joins

← INNER JOIN — matching rows across tables · FULL OUTER and CROSS JOIN — both sides, and every pair →