Module 3: Working with Multiple Tables35 min

LEFT and RIGHT OUTER JOINs

Progress Tracking

Log in to save this lesson and continue from where you left off.

Log in

The Join You’ll Use Most Often

INNER JOIN is great when you only want matches. But most analytics questions are different:

  • “Show me all customers, even if they haven’t ordered.”
  • “List every product, including ones never sold.”
  • “Find users who haven’t logged in.”

That’s LEFT JOIN territory. In practice, LEFT JOIN appears in maybe 70% of production analytics queries. Master this one.

How LEFT JOIN Works

SQL
SELECT
  e.first_name,
  d.dept_name
FROM employees e
LEFT JOIN departments d
  ON e.dept_id = d.id;
Table: customers
idfirst_namelast_namecityaddressphone_number
8JohnJosephSan Francisco928-386-8164
7JillMichaelAustin813-297-0692
4WilliamDanielDenver813-368-1200
5HenryJacksonMiami808-601-7513
13EmmaIsaacMiami808-690-5201
Table: orders
idcust_idorder_dateorder_detailstotal_order_cost
132019-03-04Coat100
232019-03-01Shoes80
332019-03-07Skirt30
472019-02-01Coat25
572019-03-10Shoes80
1
Left Join Customers with Orders

Left join customers with orders so every customer appears, even those with no orders.

Tables: customers, orders

The Anti-Join Pattern: Finding What’s Missing

This is one of the most common SQL interview patterns for data analyst roles. You’ll see it constantly:

  • “Find customers who have never placed an order.”
  • “Which products have never been sold?”
  • “List employees with no direct reports.”

The trick: LEFT JOIN, then filter for NULL on the right side.

2
Find Customers with No Orders

Add a WHERE clause to keep only customers with no matching orders. Check for NULL on the right table’s primary key.

Tables: customers, orders

After the LEFT JOIN, customers without orders have NULL in all the order columns. We use that to filter down to just the unmatched rows.

Check the primary key for NULL

Always check IS NULL on the right table’s primary key (like o.id). It’s guaranteed to be non-NULL for real matches, so NULL definitively means “no match found.”

Practice: Finding Posts by Reaction Type

Use a JOIN to connect posts with their reactions, then filter for the right reaction type.

Table: facebook_posts
post_idposterpost_textpost_keywordspost_date
02The Lakers game from last night was great.[basketball,lakers,nba]2019-01-01
11Lebron James is top class.[basketball,lebron_james,nba]2019-01-02
22Asparagus tastes OK.[asparagus,food]2019-01-01
31Spaghetti is an Italian food.[spaghetti,food]2019-01-02
43User 3 is not sharing interests[#spam#]2019-01-01
Table: facebook_reactions
posterfriendreactiondate_daypost_id
21like10
26like10
12like11
13heart11
14like11
3
Find all posts which were reacted to with a heart
View solution

Find all posts which were reacted to with a heart. For such posts output all columns from facebook_posts table.

Tables: facebook_reactions, facebook_posts

Practice: Filtering Host Units

Join the host and unit tables, then filter and aggregate.

Table: airbnb_hosts
host_idnationalitygenderage
0USAM28
1USAF29
2ChinaF31
3ChinaM24
4MaliM30
Table: airbnb_units
host_idunit_idunit_typen_bedsn_bedroomscountrycity
0A1Room11USANew York
0A2Room11USANew Jersey
0A3Room11USANew Jersey
1A4Apartment21USAHouston
1A5Apartment21USALas Vegas
4
Number Of Units Per Nationality
View solution

Write a query that returns how many different apartment-type units (counted by distinct `unit_id`) are owned by people under 30, grouped by their nationality. Sort the results by the number of apartments in descending order.

Tables: airbnb_hosts, airbnb_units

The Filter Trap: A Bug That Bites Everyone

This is one of the most common SQL bugs in production. It shows up in dashboards at multiple companies. Here’s how it happens:

WHERE on the right table breaks LEFT JOIN

Putting a WHERE filter on the right table accidentally turns your LEFT JOIN into an INNER JOIN.

Your team wants to see every customer and flag who has bought a Coat — so they can target the rest with a promotion. You need all customers in the report, with Coat order details attached where they exist.

SQL
-- THE BUG: This drops customers with no orders!
SELECT
  c.id,
  c.first_name,
  o.order_details
FROM customers c
LEFT JOIN orders o
  ON c.id = o.cust_id
WHERE o.order_details = 'Coat';

What went wrong? Customers without orders have NULL for o.order_details. And NULL = 'Coat' is not TRUE (it’s NULL), so those rows get filtered out. Your LEFT JOIN just became an INNER JOIN. The customers you wanted to target with the promo have disappeared from the report.

The Fix: Move the Filter to ON

The ON clause already contains the join condition (c.id = o.cust_id). We’re not replacing it — we’re adding the filter with AND to narrow which rows match.

Table: orders
idcust_idorder_dateorder_detailstotal_order_cost
132019-03-04Coat100
232019-03-01Shoes80
332019-03-07Skirt30
472019-02-01Coat25
572019-03-10Shoes80
Table: customers
idfirst_namelast_namecityaddressphone_number
8JohnJosephSan Francisco928-386-8164
7JillMichaelAustin813-297-0692
4WilliamDanielDenver813-368-1200
5HenryJacksonMiami808-601-7513
13EmmaIsaacMiami808-690-5201
5
Fix the Filter Trap

The query below only shows customers who bought Coats — everyone else vanishes. Move the `order_details` filter from `WHERE` into the `ON` clause so all customers appear.

Tables: customers, orders

Now the join only matches Coat orders, but customers who never bought a Coat still appear (with NULLs). That’s what you need — the full roster shows who to target with the promo.

ON vs WHERE

ON = how rows match

WHERE = which rows survive

RIGHT JOIN: The One Nobody Uses

RIGHT JOIN is the mirror image of LEFT JOIN. It keeps all rows from the right table.

In practice, almost nobody uses RIGHT JOIN. It’s harder to read (the “main” table is on the right, which feels backwards), and it can always be rewritten as a LEFT JOIN by swapping the table order:

SQL
-- These are equivalent:

-- RIGHT JOIN version:
SELECT
  c.first_name,
  o.id AS order_id
FROM customers c
RIGHT JOIN orders o
  ON c.id = o.cust_id;

-- LEFT JOIN version (preferred):
SELECT
  c.first_name,
  o.id AS order_id
FROM orders o
LEFT JOIN customers c
  ON c.id = o.cust_id;

Most teams standardize on LEFT JOIN. It’s easier to reason about when the “preserved” table is always on the left.

Key Takeaways

  • LEFT JOIN keeps all rows from the left table, filling NULLs for unmatched right columns
  • WHERE on right-table columns can accidentally break LEFT JOIN behavior — move filters to ON
  • Skip RIGHT JOIN; rewrite as LEFT JOIN with swapped tables

What’s Next

What if you need unmatched rows from both sides? “Show me customers without orders and orders without valid customers.” That’s FULL OUTER JOIN territory.