LEFT and RIGHT JOIN
Preserve all rows from one table with LEFT and RIGHT JOINs.
Introduction to LEFT and RIGHT JOINs
In the last lesson, you learned about INNER JOIN, which returns only rows that match in both tables. But what if you want to keep all rows from one table, even if there's no match in the other?
For example:
- List all customers and their orders (including customers with no orders)
- Show all products with their sales (including products never sold)
- Display all employees with their assigned projects (including those not assigned yet)
That's where LEFT JOIN and RIGHT JOIN come in - they preserve rows from one table even when there's no match!
Returns all rows from the left table, plus matching rows from the right table. If there's no match, the right table's columns are filled with NULL.
Returns all rows from the right table, plus matching rows from the left table. If there's no match, the left table's columns are filled with NULL. (Less common than LEFT JOIN)
Let's work with our familiar tables:
id | name | city |
|---|---|---|
| 1 | Alice Corp | New York |
| 2 | Bob Inc | London |
| 3 | Charlie Ltd | Toronto |
| 4 | Diana Co | Sydney |
id | customer_id | order_date | total |
|---|---|---|---|
| 101 | 1 | 2024-01-15 | 500.00 |
| 102 | 2 | 2024-01-20 | 350.00 |
| 103 | 1 | 2024-02-01 | 750.00 |
| 104 | 3 | 2024-02-05 | 220.00 |
Notice: Diana Co (customer_id = 4) has placed no orders.
LEFT JOIN - Keep All Rows from Left Table
Syntax:
SELECT columns
FROM left_table
LEFT JOIN right_table
ON left_table.column = right_table.column;
The left table is the one after FROM, the right table is the one after LEFT JOIN.
-- Get ALL customers, with their orders (if any)
SELECT
c.name,
c.city,
o.id AS order_id,
o.order_date,
o.total
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id;Key difference from INNER JOIN:
- Diana Co appears in the results! 🎉
- Her order columns are NULL because she has no orders
- INNER JOIN would have excluded Diana entirely
What happened:
- SQL took ALL rows from customers (left table)
- For each customer, it looked for matching orders
- When a match was found (Alice, Bob, Charlie), it joined the order data
- When NO match was found (Diana), it kept the customer but filled order columns with NULL
Think of LEFT JOIN as: "Give me everything from the left table, and whatever matches from the right table. If nothing matches on the right, give me NULLs."
LEFT JOIN vs INNER JOIN - Side by Side
Let's compare the two with the same data:
-- INNER JOIN - only customers WITH orders
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
INNER JOIN orders o
ON c.id = o.customer_id
GROUP BY c.id, c.name;-- LEFT JOIN - ALL customers, including those without orders
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
GROUP BY c.id, c.name;LEFT JOIN includes Diana with 0 orders. INNER JOIN excluded her entirely!
Note: We used COUNT(o.id) instead of COUNT(*) because:
COUNT(*)counts all rows (Diana would show 1, not 0)COUNT(o.id)counts non-NULL order IDs (Diana has NULL, so count = 0)
Finding Rows with No Match
A common use of LEFT JOIN is to find rows that don't have a match:
-- Find customers who have NOT placed any orders
SELECT
c.id,
c.name,
c.city
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
WHERE o.id IS NULL;How this works:
- LEFT JOIN includes all customers
- Diana has no orders, so o.id is NULL
- WHERE o.id IS NULL filters to only rows where there was no match
Common pattern for finding "orphaned" records!
-- Products that have never been ordered
SELECT p.product_id, p.product_name
FROM products p
LEFT JOIN order_items oi
ON p.product_id = oi.product_id
WHERE oi.id IS NULL;RIGHT JOIN - Keep All Rows from Right Table
RIGHT JOIN is the mirror of LEFT JOIN - it keeps all rows from the right table (the one after RIGHT JOIN).
-- Get ALL orders, with customer info (if available)
SELECT
o.id AS order_id,
o.order_date,
o.total,
c.name,
c.city
FROM customers c
RIGHT JOIN orders o
ON c.id = o.customer_id;All orders are included. If an order had no matching customer (orphaned order), the customer columns would be NULL.
In practice, RIGHT JOIN is rarely used because you can rewrite it as a LEFT JOIN by swapping the tables:
-- These are equivalent:
SELECT ... FROM customers c RIGHT JOIN orders o ON c.id = o.customer_id;
SELECT ... FROM orders o LEFT JOIN customers c ON o.customer_id = c.id;Best Practice: Use LEFT JOIN and put the "main" table (the one you want all rows from) on the left (after FROM). This makes queries easier to read and understand.
RIGHT JOIN is supported for completeness, but LEFT JOIN is clearer!
LEFT JOIN with Multiple Conditions
You can add additional conditions to the ON clause:
-- All customers with their large orders (> $400) only
SELECT
c.name,
o.id AS order_id,
o.total
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
AND o.total > 400;Important difference:
- Condition in ON: affects the join (Bob/Charlie/Diana show NULL because their orders don't meet condition)
- Condition in WHERE: filters after join (would exclude Bob/Charlie/Diana entirely)
-- Compare: Using WHERE instead filters out customers
SELECT
c.name,
o.id AS order_id,
o.total
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
WHERE o.total > 400; -- This filters AFTER the joinWHERE o.total > 400 excludes customers without large orders entirely (behaves like INNER JOIN)!
LEFT JOIN with Aggregates
Combine LEFT JOIN with GROUP BY for powerful reports:
-- Customer summary including those with no orders
SELECT
c.name,
c.city,
COUNT(o.id) AS order_count,
COALESCE(SUM(o.total), 0) AS total_spent
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
GROUP BY c.id, c.name, c.city
ORDER BY total_spent DESC;Notes:
COUNT(o.id)counts non-NULL order IDs (0 for Diana)COALESCE(SUM(o.total), 0)converts NULL to 0 for customers with no orders
id | product_name | category | price |
|---|---|---|---|
| 1 | Laptop | Electronics | 999 |
| 2 | Mouse | Electronics | 29 |
| 3 | Desk Chair | Furniture | 299 |
| 4 | Desk Lamp | Furniture | 49 |
id | order_id | product_id | quantity |
|---|---|---|---|
| 1 | 101 | 1 | 2 |
| 2 | 102 | 3 | 1 |
| 3 | 103 | 1 | 1 |
-- Product sales including unsold products
SELECT
p.product_name,
p.category,
COUNT(oi.id) AS times_ordered,
COALESCE(SUM(oi.quantity), 0) AS total_sold
FROM products p
LEFT JOIN order_items oi
ON p.id = oi.product_id
GROUP BY p.id, p.product_name, p.category
ORDER BY total_sold DESC;Mouse and Desk Lamp have never been sold, but they still appear in the results!
Multiple LEFT JOINs
You can chain multiple LEFT JOINs:
id | name | department_id | manager_id |
|---|---|---|---|
| 1 | Alice | 10 | NULL |
| 2 | Bob | 10 | 1 |
| 3 | Charlie | 20 | NULL |
| 4 | Diana | NULL | NULL |
id | dept_name |
|---|---|
| 10 | Engineering |
| 20 | Marketing |
-- All employees with department and manager info (if available)
SELECT
e.name AS employee,
d.dept_name AS department,
m.name AS manager
FROM employees e
LEFT JOIN departments d
ON e.department_id = d.id
LEFT JOIN employees m
ON e.manager_id = m.id;- Alice and Charlie have no managers (NULL)
- Diana has no department (NULL) and no manager (NULL)
- All employees appear thanks to LEFT JOIN!
Common Mistakes to Avoid
Don't make these mistakes:
-
Using COUNT(*) with LEFT JOIN: ❌ Misleading!
COUNT(*)counts rows (Diana would show 1, not 0)- Use
COUNT(right_table.id)to count matches
-
Filtering NULL in WHERE instead of ON: ❌
- WHERE filters AFTER join (removes non-matching rows)
- Put conditions in ON if you want to affect the join itself
-
Not handling NULL in aggregates: ❌
SUM(o.total)returns NULL for Diana- Use
COALESCE(SUM(o.total), 0)to convert NULL to 0
-
Using RIGHT JOIN when LEFT JOIN is clearer: ❌
FROM orders o RIGHT JOIN customers cis confusing- Better:
FROM customers c LEFT JOIN orders o
-
Confusing LEFT and INNER JOIN: ❌
- LEFT JOIN keeps all left table rows
- INNER JOIN excludes non-matching rows
- Know which one you need!
-
Not specifying which table in GROUP BY: ❌
- With joins, always use table prefix:
GROUP BY c.id, c.name
- With joins, always use table prefix:
-
Assuming NULL means "no data": Could be actual NULL! ❌
- Use
WHERE right_table.primary_key IS NULLto find non-matches - Don't use
WHERE right_table.some_column IS NULL(might be NULL in matching rows too)
- Use
Practical Examples
Example 1: Sales report with inactive customers
-- Show all customers, highlight those inactive (no recent orders)
SELECT
c.id,
c.name,
COUNT(o.id) AS orders_last_year,
CASE
WHEN COUNT(o.id) = 0 THEN 'Inactive'
WHEN COUNT(o.id) < 3 THEN 'Low Activity'
ELSE 'Active'
END AS status
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
AND o.order_date >= DATE_SUB(CURRENT_DATE, INTERVAL 1 YEAR)
GROUP BY c.id, c.name
ORDER BY orders_last_year DESC;Example 2: Product inventory check
-- Find products that haven't sold in the last 30 days
SELECT
p.id,
p.product_name,
p.price,
MAX(o.order_date) AS last_sold
FROM products p
LEFT JOIN order_items oi
ON p.id = oi.product_id
LEFT JOIN orders o
ON oi.order_id = o.id
GROUP BY p.id, p.product_name, p.price
HAVING MAX(o.order_date) < DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
OR MAX(o.order_date) IS NULL;Example 3: Employee onboarding tracker
-- Employees and their assigned equipment (if any)
SELECT
e.name AS employee,
e.hire_date,
eq.item_name,
eq.serial_number
FROM employees e
LEFT JOIN equipment_assignments ea
ON e.id = ea.employee_id
LEFT JOIN equipment eq
ON ea.equipment_id = eq.id
WHERE e.hire_date >= DATE_SUB(CURRENT_DATE, INTERVAL 90 DAY)
ORDER BY e.hire_date DESC, e.name;Visual Summary
JOIN Type | Returns | Use Case |
|---|---|---|
| INNER JOIN | Only matching rows from both tables | "Show orders WITH customers" |
| LEFT JOIN | All left table rows + matches from right | "Show ALL customers, with orders if any" |
| RIGHT JOIN | All right table rows + matches from left | Same as LEFT (swap tables) - rarely used |
Key Takeaways
What you learned:
✅ LEFT JOIN returns all rows from the left table + matches from the right
✅ Non-matching rows get NULL for right table columns
✅ Use WHERE right_table.id IS NULL to find rows with no match
✅ RIGHT JOIN is the mirror of LEFT JOIN (rarely used)
✅ Use COUNT(right_table.column) not COUNT(*) to count matches
✅ COALESCE() converts NULL to a default value (e.g., 0)
✅ Conditions in ON affect the join, conditions in WHERE filter after
✅ LEFT JOIN is essential for finding missing relationships
✅ Always use table prefixes in GROUP BY and SELECT
Coming up next: We'll learn about self-joins to join a table to itself, and UNION operations to combine results from multiple queries!
Practice Exercise: Try these queries:
- List all products with their order count (including unsold products)
- Find customers who have placed NO orders
- Show all employees with their manager names (if they have a manager)
- Calculate total revenue per product (including products with $0 revenue)
- List all departments with employee count (including empty departments)