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.
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:
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
| id | first_name | order_id | total |
|---|---|---|---|
| 1 | Jennifer | 109 | 4064.75 |
| 2 | Donna | 13 | 1618.29 |
| 3 | Kenneth | NULL | NULL |
| 4 | Michael | 11 | 1518.36 |
| 4 | Michael | 87 | 2501.23 |
| 4 | Michael | 181 | 843.7 |
| 5 | Robert | 127 | 4924.23 |
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
SELECT columns
FROM left_table
LEFT JOIN right_table
ON left_table.key = right_table.key;
| Part | What it does |
|---|---|
FROM left_table | The table whose rows are all kept. "Left" only means "written first". |
LEFT JOIN right_table | The 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 JOIN | The full name of the same join. OUTER is optional in every database. |
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 result | Right-table columns |
|---|---|---|
| One match | 1 | Filled from the matching row |
| Several matches | One per match | Filled from each matching row |
| No match | 1 | All 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:
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
| result_rows | customers | matched_orders | rows_without_a_match |
|---|---|---|---|
| 236 | 150 | 200 | 36 |
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.
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
| join_type | rows_returned |
|---|---|
| LEFT JOIN | 236 |
| INNER JOIN | 200 |
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 JOIN | INNER JOIN | |
|---|---|---|
| Matching rows | Returned | Returned |
| Left rows with no match | Returned, with NULLs on the right | Dropped |
| Right rows with no match | Dropped | Dropped |
| 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:
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
| id | first_name | orders | total_spent |
|---|---|---|---|
| 1 | Jennifer | 1 | 4064.75 |
| 2 | Donna | 1 | 1618.29 |
| 3 | Kenneth | 0 | 0 |
| 4 | Michael | 3 | 4863.29 |
| 5 | Robert | 1 | 4924.23 |
| 6 | Deborah | 2 | 7403.78 |
Two details make Kenneth's row come out as 0 and 0:
| Expression | Why |
|---|---|
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. |
*. 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?
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
| id | first_name | last_name | city |
|---|---|---|---|
| 3 | Kenneth | Young | Austin |
| 9 | Sandra | Jackson | Philadelphia |
| 12 | Richard | Thomas | Charlotte |
| 14 | Jeffrey | Lopez | Houston |
| 17 | Stephanie | Mitchell | San Antonio |
| 19 | Barbara | Lee | Dallas |
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?
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
| id | name | category |
|---|---|---|
| 110 | The Manager's Path | Books |
LEFT JOIN … IS NULL vs NOT EXISTS vs NOT IN
| Method | Result | Notes |
|---|---|---|
LEFT JOIN … WHERE right.id IS NULL | Rows with no match | Easy to extend with more joins and columns. |
WHERE NOT EXISTS (SELECT 1 …) | Same rows | States 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 NULL | If the subquery returns even one NULL, NOT IN returns no rows at all. Avoid it on nullable columns. |
-- 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:
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
| version | customers_returned |
|---|---|
| condition in WHERE | 65 |
| condition in ON | 150 |
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:
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
| id | first_name | delivered_orders |
|---|---|---|
| 1 | Jennifer | 1 |
| 2 | Donna | 1 |
| 3 | Kenneth | 0 |
| 4 | Michael | 1 |
| 5 | Robert | 1 |
| 6 | Deborah | 0 |
| Condition on… | Put it in | Because |
|---|---|---|
| The right table | ON | It limits what can match and leaves unmatched left rows in place. |
| The left table | WHERE | It removes left rows you do not want at all (as u.id <= 6 does above). |
| "No match exists" | WHERE right.id IS NULL | This 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:
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
| order_id | first_name | status | method | payment_status |
|---|---|---|---|---|
| 1 | Amanda | cancelled | PayPal | pending |
| 2 | Timothy | delivered | Cryptocurrency | success |
| 2 | Timothy | delivered | Cryptocurrency | success |
| 3 | Sarah | delivered | PayPal | success |
| 3 | Sarah | delivered | Bank Transfer | success |
| 4 | Linda | processing | NULL | NULL |
| 5 | Robert | delivered | Gift Card | pending |
| 5 | Robert | delivered | Apple Pay | pending |
| 6 | Michelle | shipped | NULL | NULL |
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:
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
| version | customers |
|---|---|
| LEFT JOIN, then LEFT JOIN | 150 |
| LEFT JOIN, then INNER JOIN | 89 |
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:
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
| id | orders | reviews | joined_rows | inflated_total |
|---|---|---|---|---|
| 36 | 3 | 4 | 12 | 15445.36 |
| 83 | 4 | 3 | 12 | 29101.08 |
| 98 | 3 | 3 | 9 | 35512.56 |
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:
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
| id | first_name | orders | spent | reviews |
|---|---|---|---|---|
| 11 | Ronald | 1 | 2446.26 | 1 |
| 15 | David | 1 | 1152.13 | 0 |
| 43 | Amanda | 2 | 4018.42 | 0 |
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:
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
| query | rows_returned |
|---|---|
| users LEFT JOIN orders | 236 |
| orders RIGHT JOIN users | 236 |
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:
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
| id | first_name | title | manager |
|---|---|---|---|
| 8 | Steven | Paralegal | NULL |
| 9 | Michael | Senior Counsel | NULL |
| 10 | Michelle | COO | NULL |
| 11 | Laura | Sales Manager | William |
| 12 | Betty | COO | William |
| 13 | Joshua | Content Lead | Michelle |
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.
| Field | Question | Left table → right table |
|---|---|---|
| E-commerce | Customers who signed up but never ordered | customers → orders |
| Retail | Products that have never sold | products → sales |
| HR | Employees with no performance review this cycle | employees → reviews |
| Education | Students not enrolled in any course | students → enrollments |
| Marketing | Leads that never converted | leads → purchases |
| CRM | Contacts with no logged activity | contacts → activities |
| Warehouse | Stock with no sales this quarter (date condition in ON) | inventory → sales |
| Promotions | Coupons issued but never redeemed | coupons → redemptions |
| Product | Accounts that have never logged in | users → logins |
| Banking | Accounts with no transactions in the period | accounts → transactions |
| Insurance | Policies with no claims | policies → claims |
| Healthcare | Patients who never booked an appointment | patients → appointments |
| Analytics | Carts that did not become orders (abandonment) | carts → orders |
| Reporting | Every month on a chart, including months with no sales | calendar → sales |
| Dashboards | All customers with order counts, zeros included | customers → 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
SELECT u.id, o.id
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
WHERE o.status = 'delivered';
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 =
SELECT u.id
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
WHERE o.id = NULL;
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
SELECT u.id, COUNT(*) AS orders
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
GROUP BY u.id;
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
SELECT u.first_name, o.id
FROM orders o
LEFT JOIN users u
ON u.id = o.user_id;
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
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;
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
| Tip | Why it helps |
|---|---|
| Index the join column of the right table | For 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 early | Fewer left rows means fewer lookups. A WHERE condition on the left table is safe. |
| Select only the columns you need | SELECT * across several joins reads and returns far more data than a report uses. |
| Use LEFT JOIN only where rows can be missing | If every row is guaranteed a match, an INNER JOIN says so and gives the optimiser more freedom. |
| Check the plan | EXPLAIN (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
| Goal | Pattern |
|---|---|
| All left rows, with matches if any | FROM 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 included | COUNT(b.id) with GROUP BY a.id |
| Replace NULL in the output | COALESCE(b.value, 0) |
| Filter which right rows may match | Add the condition to ON, not WHERE |
| Three or more tables | One LEFT JOIN … ON per table |
| Same as RIGHT JOIN | Swap 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.
-
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, ordersShow solution
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.
-
2Easy Find the products that have no reviews. Show the id, name and category. Table:
products, reviewsShow solution
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 NULLkeeps only the unmatched ones: 27 products. -
3Medium Show every customer with the number of orders they have placed, highest first. Customers with none should show 0. Table:
users, ordersShow solution
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. -
4Medium Which orders have no payment recorded? Show the order id, status and total, largest total first. Table:
orders, paymentsShow solution
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
ordersas the table to keep. 75 orders have no payment row. -
5Medium For each product category, count the products that have no reviews. Table:
products, reviewsShow solution
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. -
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, ordersShow solution
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.
-
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, employeesShow solution
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
departmentsis the left table because the report is "per department". The salary condition belongs in ON for the same reason as in the previous exercise. -
8Challenging Find customers who have placed at least one order but have never written a review. Show each customer once. Table:
users, orders, reviewsShow solution
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
What does a LEFT JOIN return?
Every left-table row is returned. Matching right-table rows are attached, and unmatched left rows get NULL in the right-table columns.
A customer has no orders. What does users LEFT JOIN orders show for that customer?
The customer appears once, and every column that comes from orders is NULL.
Is LEFT OUTER JOIN different from LEFT JOIN?
LEFT JOIN is short for LEFT OUTER JOIN. Both are the same join in every database.
Which query finds customers with no orders?
LEFT JOIN keeps every customer and IS NULL on the right table's key keeps only those with no match. "= NULL" is never true.
users LEFT JOIN orders … WHERE o.status = 'delivered'. What happens to customers with no orders?
For those customers o.status is NULL, so the WHERE condition is not true and the rows are removed. Put the condition in ON to keep them.
A table has 100 rows. What is the smallest number of rows a LEFT JOIN from it can return (with no WHERE clause)?
Every left row appears at least once, so the result has at least 100 rows. It has more if some rows match several times.
After users LEFT JOIN orders, which expression gives 0 for customers with no orders?
COUNT(o.id) counts only non-NULL order ids. The other three count the customer's single NULL row as 1.
Which table is the "left" table?
Left simply means first: the table already in the FROM clause when LEFT JOIN is written.
How do you rewrite A RIGHT JOIN B as a LEFT JOIN?
RIGHT JOIN keeps every row of B, so B becomes the left table: B LEFT JOIN A.
A query LEFT JOINs users to orders and also to reviews, then sums o.total. Why is the total too high?
Two one-to-many joins on the same parent pair every order with every review. Aggregate each table in a subquery first, then join.
A LEFT JOIN is followed by an INNER JOIN on a column of the right table. What is the effect?
The INNER JOIN cannot match the NULL rows, so they disappear. Use LEFT JOIN for the later joins as well.
Why is NOT IN risky for finding rows with no match?
x NOT IN (…, NULL) is never true, so the query returns nothing. LEFT JOIN … IS NULL and NOT EXISTS do not have this problem.
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.