SQLab Hub
Lesson 14 · LEFT JOIN
Lesson 14 · Querying

SQL LEFT JOIN: Syntax, Examples & Practice

SQL LEFT JOIN returns every row from the left table, plus the matching rows from the right table. Where a left row has no match, it still appears, with NULL in the right table's columns. LEFT OUTER JOIN is the same thing.

⟕ Every left row kept ▶ Runnable Playground examples 🔍 Find rows with no match 🧪 8 practice exercises 📝 12-question quiz 🎤 15 interview questions

What Is a LEFT JOIN in SQL?

The Playground database has a users table of customers and an orders table. Not every customer has ordered. To list customers with their orders without losing the ones who never bought anything, join the two tables with LEFT JOIN:

SQLite · runs in the Playground
SELECT u.id, u.first_name, o.id AS order_id, o.total
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
WHERE u.id <= 5
ORDER BY u.id, o.id;

▶ Run this query in the SQL Playground

The first five customers and their orders
idfirst_nameorder_idtotal
1Jennifer1094064.75
2Donna131618.29
3KennethNULLNULL
4Michael111518.36
4Michael872501.23
4Michael181843.7
5Robert1274924.23
✓ 7 rows · real output from the Playground database

Read the result row by row. Jennifer and Donna have one order each, so each gets one row. Kenneth has no orders, but he is still there: the two columns that come from orders are NULL. Michael has three orders, so he appears three times. Five customers went in and 7 rows came out.

That is the whole idea. users is written first, so it is the left table and all of its rows are kept. orders is the right table: its rows are attached where the ON condition is true, and replaced by NULLs where it is not. An INNER JOIN would have dropped Kenneth.

LEFT JOIN Syntax

SQL · syntax
SELECT columns
FROM left_table
LEFT JOIN right_table
  ON left_table.key = right_table.key;
PartWhat it does
FROM left_tableThe table whose rows are all kept. "Left" only means "written first".
LEFT JOIN right_tableThe table to look up matches in. Only matching rows are attached.
ON …The matching rule, usually a foreign key compared with the primary key it points to.
LEFT OUTER JOINThe full name of the same join. OUTER is optional in every database.
Same syntax everywhere. LEFT JOIN is standard SQL and is written identically in MySQL, PostgreSQL, SQL Server, Oracle and SQLite. The examples on this page run in the SQLite Playground and work unchanged in the others.

How LEFT JOIN Works, Row by Row

For each row of the left table, the database looks for rows in the right table that satisfy ON:

Left row has…Rows in the resultRight-table columns
One match1Filled from the matching row
Several matchesOne per matchFilled from each matching row
No match1All NULL

So a LEFT JOIN can never return fewer rows than the left table, but it can return more. Counting the full join of customers and orders shows all three cases at once:

SQLite · runs in the Playground
SELECT COUNT(*)             AS result_rows,
       COUNT(DISTINCT u.id) AS customers,
       COUNT(o.id)          AS matched_orders,
       COUNT(*) - COUNT(o.id) AS rows_without_a_match
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id;

▶ Run this query in the SQL Playground

Row counts of users LEFT JOIN orders
result_rowscustomersmatched_ordersrows_without_a_match
23615020036
✓ 1 row · real output from the Playground database

All 150 customers are present. The 200 orders each produce a row, and the 36 customers who never ordered add one NULL row each: 200 + 36 = 236 rows.

LEFT JOIN vs INNER JOIN

The only difference is what happens to left rows that have no match. INNER JOIN drops them; LEFT JOIN keeps them.

SQLite · runs in the Playground
SELECT 'LEFT JOIN' AS join_type, COUNT(*) AS rows_returned
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
UNION ALL
SELECT 'INNER JOIN', COUNT(*)
FROM users u
INNER JOIN orders o ON o.user_id = u.id;

▶ Run this query in the SQL Playground

Rows returned by LEFT JOIN and INNER JOIN on the same tables
join_typerows_returned
LEFT JOIN236
INNER JOIN200
✓ 2 rows · real output from the Playground database

The 36-row gap is exactly the customers without orders. A "number of customers" report built on the INNER JOIN would quietly leave them out.

LEFT JOININNER JOIN
Matching rowsReturnedReturned
Left rows with no matchReturned, with NULLs on the rightDropped
Right rows with no matchDroppedDropped
Use it for"All customers, with their orders if any""Only customers who have ordered"

The SQL JOINs overview compares all the join types side by side.

Handling the NULLs: COUNT and COALESCE

NULL in a LEFT JOIN result means "no matching row". It is not zero and not an empty string, which matters as soon as you aggregate. This report shows orders and spending per customer, including customers with none:

SQLite · runs in the Playground
SELECT u.id, u.first_name,
       COUNT(o.id) AS orders,
       COALESCE(ROUND(SUM(o.total), 2), 0) AS total_spent
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
WHERE u.id <= 6
GROUP BY u.id, u.first_name
ORDER BY u.id;

▶ Run this query in the SQL Playground

Orders and total spent per customer, including customers with no orders
idfirst_nameorderstotal_spent
1Jennifer14064.75
2Donna11618.29
3Kenneth00
4Michael34863.29
5Robert14924.23
6Deborah27403.78
✓ 6 rows · real output from the Playground database

Two details make Kenneth's row come out as 0 and 0:

ExpressionWhy
COUNT(o.id)Counts only non-NULL values, so an unmatched customer scores 0. COUNT(*) would count the NULL row and report 1.
COALESCE(SUM(o.total), 0)SUM over no rows is NULL. COALESCE replaces it with 0.
Count a right-table column, not *. After a LEFT JOIN, COUNT(*) can never be 0, because every left row produces at least one result row.

Finding Rows With No Match: LEFT JOIN … IS NULL

The most common practical use of LEFT JOIN, and a standard interview question.

Unmatched rows are the ones whose right-table columns are NULL. Keep only those with IS NULL and you have the rows that have no partner. Which customers have never placed an order?

SQLite · runs in the Playground
SELECT u.id, u.first_name, u.last_name, u.city
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
WHERE o.id IS NULL
ORDER BY u.id;

▶ Run this query in the SQL Playground

Customers who have never placed an order
idfirst_namelast_namecity
3KennethYoungAustin
9SandraJacksonPhiladelphia
12RichardThomasCharlotte
14JeffreyLopezHouston
17StephanieMitchellSan Antonio
19BarbaraLeeDallas
✓ 36 rows · first 6 shown · real output from the Playground database

36 customers have no orders. Test a right-table column that can never be NULL in a real row, normally its primary key (o.id). If you test a column that is allowed to be NULL, a matched row with an empty value would be mistaken for a missing one.

The same pattern answers any "which X have no Y" question. Which product has never been ordered?

SQLite · runs in the Playground
SELECT p.id, p.name, p.category
FROM products p
LEFT JOIN order_items oi
  ON oi.product_id = p.id
WHERE oi.id IS NULL;

▶ Run this query in the SQL Playground

Products that appear in no order
idnamecategory
110The Manager's PathBooks
✓ 1 row · real output from the Playground database

LEFT JOIN … IS NULL vs NOT EXISTS vs NOT IN

MethodResultNotes
LEFT JOIN … WHERE right.id IS NULLRows with no matchEasy to extend with more joins and columns.
WHERE NOT EXISTS (SELECT 1 …)Same rowsStates the intent directly. Databases usually plan it as well as, or better than, the join form.
WHERE id NOT IN (SELECT …)Same rows only if the subquery returns no NULLIf the subquery returns even one NULL, NOT IN returns no rows at all. Avoid it on nullable columns.
SQL
-- the same question with NOT EXISTS
SELECT u.id, u.first_name
FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

Both forms return the same 36 customers here. See EXISTS and NOT EXISTS for the subquery version in depth.

LEFT JOIN With a WHERE Clause: ON vs WHERE

The mistake that silently turns a LEFT JOIN into an INNER JOIN.

Suppose you want every customer with the number of delivered orders. Where does the condition o.status = 'delivered' go? It makes a large difference:

SQLite · runs in the Playground
SELECT 'condition in WHERE' AS version, COUNT(DISTINCT u.id) AS customers_returned
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.status = 'delivered'
UNION ALL
SELECT 'condition in ON', COUNT(DISTINCT u.id)
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'delivered';

▶ Run this query in the SQL Playground

Customers returned when the filter is in WHERE and when it is in ON
versioncustomers_returned
condition in WHERE65
condition in ON150
✓ 2 rows · real output from the Playground database

With the condition in WHERE, only 65 of 150 customers survive. WHERE runs after the join, and for an unmatched customer o.status is NULL, so NULL = 'delivered' is not true and the row is removed. The LEFT JOIN has become an INNER JOIN.

With the condition in ON, it only decides which orders count as a match. Every customer is kept:

SQLite · runs in the Playground
SELECT u.id, u.first_name,
       COUNT(o.id) AS delivered_orders
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
 AND o.status = 'delivered'
WHERE u.id <= 6
GROUP BY u.id, u.first_name
ORDER BY u.id;

▶ Run this query in the SQL Playground

Delivered orders per customer, including customers with none
idfirst_namedelivered_orders
1Jennifer1
2Donna1
3Kenneth0
4Michael1
5Robert1
6Deborah0
✓ 6 rows · real output from the Playground database
Condition on…Put it inBecause
The right tableONIt limits what can match and leaves unmatched left rows in place.
The left tableWHEREIt removes left rows you do not want at all (as u.id <= 6 does above).
"No match exists"WHERE right.id IS NULLThis is the one right-table test that belongs in WHERE.

LEFT JOIN on Multiple Tables

Add one LEFT JOIN … ON per table. Each join works on the result of the previous one. Here every order is listed with its customer and any payments recorded for it:

SQLite · runs in the Playground
SELECT o.id AS order_id, u.first_name, o.status,
       p.method, p.status AS payment_status
FROM orders o
LEFT JOIN users u
  ON u.id = o.user_id
LEFT JOIN payments p
  ON p.order_id = o.id
WHERE o.id <= 6
ORDER BY o.id, p.id;

▶ Run this query in the SQL Playground

Orders with their customer and payments
order_idfirst_namestatusmethodpayment_status
1AmandacancelledPayPalpending
2TimothydeliveredCryptocurrencysuccess
2TimothydeliveredCryptocurrencysuccess
3SarahdeliveredPayPalsuccess
3SarahdeliveredBank Transfersuccess
4LindaprocessingNULLNULL
5RobertdeliveredGift Cardpending
5RobertdeliveredApple Paypending
6MichelleshippedNULLNULL
✓ 9 rows · real output from the Playground database

Orders 4 and 6 have no payment, so their payment columns are NULL, but the orders are not lost. Orders 2, 3 and 5 have two payments each and appear twice.

Do not put an INNER JOIN after a LEFT JOIN by accident

A later INNER JOIN that depends on the right table throws away the NULL rows the LEFT JOIN kept:

SQLite · runs in the Playground
SELECT 'LEFT JOIN, then LEFT JOIN' AS version, COUNT(DISTINCT u.id) AS customers
FROM users u
LEFT JOIN orders o   ON o.user_id = u.id
LEFT JOIN payments p ON p.order_id = o.id
UNION ALL
SELECT 'LEFT JOIN, then INNER JOIN', COUNT(DISTINCT u.id)
FROM users u
LEFT JOIN orders o    ON o.user_id = u.id
INNER JOIN payments p ON p.order_id = o.id;

▶ Run this query in the SQL Playground

Customers kept by two LEFT JOINs and by a LEFT JOIN followed by an INNER JOIN
versioncustomers
LEFT JOIN, then LEFT JOIN150
LEFT JOIN, then INNER JOIN89
✓ 2 rows · real output from the Playground database

The second version keeps only 89 customers: the ones with at least one paid order. If the first table must stay complete, every join after it has to be a LEFT JOIN too.

Why LEFT JOIN Returns Duplicate Rows

A LEFT JOIN does not create duplicates on its own. A left row is repeated because it has several matches, which is correct for a one-to-many relationship. The trouble starts when you join two "many" tables to the same parent, such as a customer's orders and a customer's reviews. Every order is paired with every review:

SQLite · runs in the Playground
SELECT u.id,
       COUNT(DISTINCT o.id) AS orders,
       COUNT(DISTINCT r.id) AS reviews,
       COUNT(*)             AS joined_rows,
       ROUND(SUM(o.total), 2) AS inflated_total
FROM users u
LEFT JOIN orders o  ON o.user_id = u.id
LEFT JOIN reviews r ON r.user_id = u.id
GROUP BY u.id
HAVING COUNT(DISTINCT o.id) > 1 AND COUNT(DISTINCT r.id) > 1
ORDER BY joined_rows DESC, u.id
LIMIT 3;

▶ Run this query in the SQL Playground

Customers whose orders and reviews multiply each other
idordersreviewsjoined_rowsinflated_total
36341215445.36
83431229101.08
9833935512.56
✓ 3 rows · real output from the Playground database

Customer 36 has 3 orders and 4 reviews, which become 12 joined rows, so SUM(o.total) adds every order 4 times. The fix is to summarise each child table first in a subquery, then LEFT JOIN the summaries, which have one row per customer:

SQLite · runs in the Playground
SELECT u.id, u.first_name,
       COALESCE(o.orders, 0)  AS orders,
       COALESCE(o.spent, 0)   AS spent,
       COALESCE(r.reviews, 0) AS reviews
FROM users u
LEFT JOIN (
  SELECT user_id, COUNT(*) AS orders, ROUND(SUM(total), 2) AS spent
  FROM orders
  GROUP BY user_id
) AS o ON o.user_id = u.id
LEFT JOIN (
  SELECT user_id, COUNT(*) AS reviews
  FROM reviews
  GROUP BY user_id
) AS r ON r.user_id = u.id
WHERE u.id IN (11, 15, 43)
ORDER BY u.id;

▶ Run this query in the SQL Playground

Orders, spending and reviews per customer with no double counting
idfirst_nameordersspentreviews
11Ronald12446.261
15David11152.130
43Amanda24018.420
✓ 3 rows · real output from the Playground database

LEFT JOIN vs RIGHT JOIN

A RIGHT JOIN keeps every row of the table written second. Swap the table order and it is the same query:

SQLite · runs in the Playground
SELECT 'users LEFT JOIN orders' AS query, COUNT(*) AS rows_returned
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
UNION ALL
SELECT 'orders RIGHT JOIN users', COUNT(*)
FROM orders o
RIGHT JOIN users u ON o.user_id = u.id;

▶ Run this query in the SQL Playground

A LEFT JOIN and the equivalent RIGHT JOIN return the same number of rows
queryrows_returned
users LEFT JOIN orders236
orders RIGHT JOIN users236
✓ 2 rows · real output from the Playground database

Most teams write LEFT JOIN only, so the table being kept is always the first one you read. To keep unmatched rows from both sides, use a FULL OUTER JOIN.

LEFT JOIN a Table to Itself

In employees, manager_id points at another employee. Joining the table to itself lists each person with their manager, and LEFT JOIN keeps the people at the top who have none:

SQLite · runs in the Playground
SELECT e.id, e.first_name, e.title,
       m.first_name AS manager
FROM employees e
LEFT JOIN employees m
  ON m.id = e.manager_id
WHERE e.id BETWEEN 8 AND 13
ORDER BY e.id;

▶ Run this query in the SQL Playground

Employees with their manager, including employees with no manager
idfirst_nametitlemanager
8StevenParalegalNULL
9MichaelSenior CounselNULL
10MichelleCOONULL
11LauraSales ManagerWilliam
12BettyCOOWilliam
13JoshuaContent LeadMichelle
✓ 6 rows · real output from the Playground database

The two aliases, e and m, let one table play both roles. The SELF JOIN lesson covers this pattern.

Where LEFT JOIN Is Used at Work

Most business uses are one of two shapes: "everything, with details where they exist" or "the ones with nothing". The tables change; the query does not.

FieldQuestionLeft table → right table
E-commerceCustomers who signed up but never orderedcustomers → orders
RetailProducts that have never soldproducts → sales
HREmployees with no performance review this cycleemployees → reviews
EducationStudents not enrolled in any coursestudents → enrollments
MarketingLeads that never convertedleads → purchases
CRMContacts with no logged activitycontacts → activities
WarehouseStock with no sales this quarter (date condition in ON)inventory → sales
PromotionsCoupons issued but never redeemedcoupons → redemptions
ProductAccounts that have never logged inusers → logins
BankingAccounts with no transactions in the periodaccounts → transactions
InsurancePolicies with no claimspolicies → claims
HealthcarePatients who never booked an appointmentpatients → appointments
AnalyticsCarts that did not become orders (abandonment)carts → orders
ReportingEvery month on a chart, including months with no salescalendar → sales
DashboardsAll customers with order counts, zeros includedcustomers → orders

BI tools use the same join under different names: "Left Outer" in Power Query and Excel merges, and the left join type in Tableau.

Common LEFT JOIN Mistakes

Five errors that return wrong results without any error message.

1. Filtering the right table in WHERE

❌ Drops customers with no delivered order
SQL
SELECT u.id, o.id
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
WHERE o.status = 'delivered';
✅ Condition moved into ON
SQL
SELECT u.id, o.id
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
 AND o.status = 'delivered';

A WHERE condition on a right-table column is never true for the NULL rows, so they are removed and the join behaves like an INNER JOIN. See ON vs WHERE.

2. Testing for NULL with =

❌ Returns no rows
SQL
SELECT u.id
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
WHERE o.id = NULL;
✅ IS NULL
SQL
SELECT u.id
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
WHERE o.id IS NULL;

NULL is not equal to anything, including NULL, so = NULL is never true. Use IS NULL.

3. COUNT(*) after a LEFT JOIN

❌ Customers with no orders show 1
SQL
SELECT u.id, COUNT(*) AS orders
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
GROUP BY u.id;
✅ Count a right-table column
SQL
SELECT u.id, COUNT(o.id) AS orders
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
GROUP BY u.id;

COUNT(*) counts rows, and an unmatched customer still has one row. COUNT(o.id) ignores the NULL and returns 0.

4. The wrong table on the left

❌ Keeps every order, not every customer
SQL
SELECT u.first_name, o.id
FROM orders o
LEFT JOIN users u
  ON u.id = o.user_id;
✅ The table to keep goes first
SQL
SELECT u.first_name, o.id
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id;

LEFT JOIN protects the table written before it. If the report must contain every customer, users has to be the left table.

5. Summing across two one-to-many joins

❌ Totals multiplied
SQL
SELECT u.id, SUM(o.total)
FROM users u
LEFT JOIN orders o  ON o.user_id = u.id
LEFT JOIN reviews r ON r.user_id = u.id
GROUP BY u.id;
✅ Aggregate first, then join
SQL
SELECT u.id, o.spent
FROM users u
LEFT JOIN (
  SELECT user_id, SUM(total) AS spent
  FROM orders GROUP BY user_id
) AS o ON o.user_id = u.id;

Each order is repeated once per review, so the sum is too high. Summarise each child table before joining, as shown in Duplicates.

LEFT JOIN Performance

TipWhy it helps
Index the join column of the right tableFor each left row the database looks up matches by that column. Primary keys are indexed already; foreign keys such as orders.user_id usually need an index of their own.
Filter the left table earlyFewer left rows means fewer lookups. A WHERE condition on the left table is safe.
Select only the columns you needSELECT * across several joins reads and returns far more data than a report uses.
Use LEFT JOIN only where rows can be missingIf every row is guaranteed a match, an INNER JOIN says so and gives the optimiser more freedom.
Check the planEXPLAIN (or EXPLAIN QUERY PLAN in SQLite) shows whether an index is used for the lookup.

On the small Playground tables every query is instant. These points matter once tables reach hundreds of thousands of rows.

LEFT JOIN Cheat Sheet

GoalPattern
All left rows, with matches if anyFROM a LEFT JOIN b ON b.a_id = a.id
Left rows with no match… LEFT JOIN b ON … WHERE b.id IS NULL
Count matches, zeros includedCOUNT(b.id) with GROUP BY a.id
Replace NULL in the outputCOALESCE(b.value, 0)
Filter which right rows may matchAdd the condition to ON, not WHERE
Three or more tablesOne LEFT JOIN … ON per table
Same as RIGHT JOINSwap the two tables

Practice LEFT JOIN Queries

8 exercises on the real Playground tables.

Write each query yourself first. Every solution has been run against the Playground database.

  1. 1Easy List the first ten customers by id with the id and status of each of their orders. Customers with no orders must still appear. Table: users, orders

    Show solution
    SQLite · runs in the Playground
    SELECT u.id, u.first_name, o.id AS order_id, o.status
    FROM users u
    LEFT JOIN orders o
      ON o.user_id = u.id
    WHERE u.id <= 10
    ORDER BY u.id, o.id;

    ▶ Run this query in the SQL Playground

    The filter is on the left table, so it is safe in WHERE. The result has 18 rows because some customers have several orders and some have none.

  2. 2Easy Find the products that have no reviews. Show the id, name and category. Table: products, reviews

    Show solution
    SQLite · runs in the Playground
    SELECT p.id, p.name, p.category
    FROM products p
    LEFT JOIN reviews r
      ON r.product_id = p.id
    WHERE r.id IS NULL
    ORDER BY p.id;

    ▶ Run this query in the SQL Playground

    LEFT JOIN keeps every product and r.id IS NULL keeps only the unmatched ones: 27 products.

  3. 3Medium Show every customer with the number of orders they have placed, highest first. Customers with none should show 0. Table: users, orders

    Show solution
    SQLite · runs in the Playground
    SELECT u.id, u.first_name, u.last_name,
           COUNT(o.id) AS orders
    FROM users u
    LEFT JOIN orders o
      ON o.user_id = u.id
    GROUP BY u.id, u.first_name, u.last_name
    ORDER BY orders DESC, u.id;

    ▶ Run this query in the SQL Playground

    COUNT(o.id) skips the NULL of an unmatched customer, so they score 0. All 150 customers are returned.

  4. 4Medium Which orders have no payment recorded? Show the order id, status and total, largest total first. Table: orders, payments

    Show solution
    SQLite · runs in the Playground
    SELECT o.id, o.status, o.total
    FROM orders o
    LEFT JOIN payments p
      ON p.order_id = o.id
    WHERE p.id IS NULL
    ORDER BY o.total DESC;

    ▶ Run this query in the SQL Playground

    The anti-join pattern again, with orders as the table to keep. 75 orders have no payment row.

  5. 5Medium For each product category, count the products that have no reviews. Table: products, reviews

    Show solution
    SQLite · runs in the Playground
    SELECT p.category,
           COUNT(*) AS products_without_reviews
    FROM products p
    LEFT JOIN reviews r
      ON r.product_id = p.id
    WHERE r.id IS NULL
    GROUP BY p.category
    ORDER BY products_without_reviews DESC, p.category;

    ▶ Run this query in the SQL Playground

    After the IS NULL filter each remaining row is one unreviewed product, so COUNT(*) is correct here.

  6. 6Medium Show every customer with the number of cancelled orders they have, including customers with none. Return the ten customers with the most. Table: users, orders

    Show solution
    SQLite · runs in the Playground
    SELECT u.id, u.first_name,
           COUNT(o.id) AS cancelled_orders
    FROM users u
    LEFT JOIN orders o
      ON o.user_id = u.id
     AND o.status = 'cancelled'
    GROUP BY u.id, u.first_name
    ORDER BY cancelled_orders DESC, u.id
    LIMIT 10;

    ▶ Run this query in the SQL Playground

    The status condition is in ON, so it limits which orders match without removing customers. In WHERE it would drop everyone with no cancelled order.

  7. 7Challenging For each department, count the employees who earn more than 150,000. Every department must appear, even if the count is 0. Table: departments, employees

    Show solution
    SQLite · runs in the Playground
    SELECT d.name,
           COUNT(e.id) AS high_earners
    FROM departments d
    LEFT JOIN employees e
      ON e.department_id = d.id
     AND e.salary > 150000
    GROUP BY d.id, d.name
    ORDER BY high_earners DESC, d.name;

    ▶ Run this query in the SQL Playground

    departments is the left table because the report is "per department". The salary condition belongs in ON for the same reason as in the previous exercise.

  8. 8Challenging Find customers who have placed at least one order but have never written a review. Show each customer once. Table: users, orders, reviews

    Show solution
    SQLite · runs in the Playground
    SELECT DISTINCT u.id, u.first_name, u.last_name
    FROM users u
    INNER JOIN orders o
      ON o.user_id = u.id
    LEFT JOIN reviews r
      ON r.user_id = u.id
    WHERE r.id IS NULL
    ORDER BY u.id;

    ▶ Run this query in the SQL Playground

    INNER JOIN keeps only customers with orders; the LEFT JOIN with IS NULL then removes the ones who have reviewed. DISTINCT collapses customers with several orders. 30 customers qualify.

📝 Quiz — LEFT JOIN

Test your understanding · 0/12 answered

Question 1 of 12

What does a LEFT JOIN return?

Question 2 of 12

A customer has no orders. What does users LEFT JOIN orders show for that customer?

Question 3 of 12

Is LEFT OUTER JOIN different from LEFT JOIN?

Question 4 of 12

Which query finds customers with no orders?

Question 5 of 12

users LEFT JOIN orders … WHERE o.status = 'delivered'. What happens to customers with no orders?

Question 6 of 12

A table has 100 rows. What is the smallest number of rows a LEFT JOIN from it can return (with no WHERE clause)?

Question 7 of 12

After users LEFT JOIN orders, which expression gives 0 for customers with no orders?

Question 8 of 12

Which table is the "left" table?

Question 9 of 12

How do you rewrite A RIGHT JOIN B as a LEFT JOIN?

Question 10 of 12

A query LEFT JOINs users to orders and also to reviews, then sums o.total. Why is the total too high?

Question 11 of 12

A LEFT JOIN is followed by an INNER JOIN on a column of the right table. What is the effect?

Question 12 of 12

Why is NOT IN risky for finding rows with no match?

LEFT JOIN Interview Questions

Q1. What is a LEFT JOIN?

A join that returns every row from the left (first) table and the matching rows from the right table. Left rows with no match are still returned, with NULL in the right table's columns. LEFT OUTER JOIN is the same join.

Q2. What is the difference between LEFT JOIN and INNER JOIN?

INNER JOIN returns only rows that match in both tables. LEFT JOIN also returns the left rows that have no match. On tables where every left row has a match, the two return the same result.

Q3. How do you find rows in one table that have no match in another?

LEFT JOIN the second table and filter with WHERE right_table.id IS NULL, testing a column that cannot be NULL in a real row. NOT EXISTS gives the same result and is often planned at least as efficiently. NOT IN is unsafe when the subquery can return NULL.

Q4. What is the difference between a condition in ON and in WHERE for a LEFT JOIN?

ON decides which right rows match; left rows are kept either way. WHERE filters the joined result, so a condition on a right-table column removes the unmatched rows (their value is NULL) and the query behaves like an INNER JOIN.

Q5. Can a LEFT JOIN return more rows than the left table has?

Yes. A left row with several matches is returned once per match. It can never return fewer rows than the left table unless a WHERE clause removes them.

Q6. Why does a LEFT JOIN produce NULL values?

When a left row has no matching right row there is no data to show, so the right table's columns are NULL. NULLs can also come from the data itself, which is why the anti-join tests a non-nullable column such as the primary key.

Q7. How do you count related rows and include zeros?

LEFT JOIN, GROUP BY the left table's key and use COUNT(right_table.id). COUNT(*) would return 1 for a left row with no match, because that row still exists in the result.

Q8. What is the difference between LEFT JOIN and RIGHT JOIN?

They are mirror images. LEFT JOIN keeps all rows of the first table, RIGHT JOIN all rows of the second. A RIGHT JOIN B is the same as B LEFT JOIN A.

Q9. How do you LEFT JOIN three tables?

Write one LEFT JOIN … ON per table. Each join is applied to the result so far. If a later join is an INNER JOIN on a column of an earlier right table, the NULL rows are lost.

Q10. What happens when the join column in the left table is NULL?

NULL never equals anything, so the row finds no match. A LEFT JOIN still returns it, with NULLs on the right; an INNER JOIN would drop it.

Q11. Why did my totals double after adding a second LEFT JOIN?

Both joined tables have several rows per parent, so every row of one is paired with every row of the other. Aggregate each table in a subquery or CTE first and join the one-row-per-parent results.

Q12. How would you list every month with its sales, including months with none?

Start from a table (or generated list) of months and LEFT JOIN the sales to it, then use COALESCE(SUM(amount), 0). Starting from sales would leave the empty months out.

Q13. How do you get only the latest related row for each left row?

Join to a subquery that picks one row per key, for example with ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) filtered to 1, or with MAX(order_date) grouped by key. LEFT JOIN keeps the left rows that have none.

Q14. How does a LEFT JOIN affect performance?

The cost is mostly the lookups into the right table, so an index on its join column matters most. LEFT JOIN also limits the join orders the optimiser may choose, so use it only where rows can really be missing.

Q15. How do you find customers who ordered last month but not this month?

Take last month's customers as the left side and LEFT JOIN this month's orders on the customer id, with the date range in ON. Then keep rows where this month's order id IS NULL.

Frequently Asked Questions

What is LEFT JOIN in SQL?

LEFT JOIN returns every row from the left table and the matching rows from the right table. When a left row has no match, it is still returned, with NULL in the right table's columns.

Is LEFT JOIN the same as LEFT OUTER JOIN?

Yes. LEFT JOIN is the short form of LEFT OUTER JOIN. The OUTER keyword is optional in MySQL, PostgreSQL, SQL Server, Oracle and SQLite, and the result is identical.

What is the difference between LEFT JOIN and INNER JOIN?

INNER JOIN returns only rows that have a match in both tables. LEFT JOIN returns those rows and also the left-table rows with no match, filling the right-table columns with NULL.

How do I find rows that have no match with LEFT JOIN?

LEFT JOIN the other table and add WHERE right_table.id IS NULL. Only the left rows that found no match remain. NOT EXISTS returns the same rows.

Why does my LEFT JOIN behave like an INNER JOIN?

Usually because the WHERE clause tests a column of the right table. That condition is not true for the NULL rows of unmatched left rows, so they are removed. Move the condition into the ON clause.

Why does LEFT JOIN return duplicate rows?

A left row is returned once for each matching right row, so a customer with three orders appears three times. Group the result, or aggregate the right table in a subquery before joining, if you need one row per left row.

Can I LEFT JOIN more than two tables?

Yes. Add one LEFT JOIN … ON clause for each additional table. Keep the later joins as LEFT JOINs too if the first table must stay complete.

Should I use LEFT JOIN or RIGHT JOIN?

They do the same job from opposite sides, and any RIGHT JOIN can be written as a LEFT JOIN by swapping the tables. Most people use LEFT JOIN throughout so the table being kept is always the first one.