Joins: Intermediate
Twitter's recommendation engine surfaces thousands of new accounts to every user each week, and generating those suggestions requires showing results even when follow data is incomplete. When a user has zero mutual followers with a suggested account, Twitter still needs to include that account with a zero-connection count rather than hiding it entirely. That behavior is powered by outer joins, which preserve rows from one table even when the other table has nothing to match. The join types you are about to learn are the engine behind every "who to follow" feature on social platforms.
LEFT JOIN
Keep all rows even without a match
LEFT JOIN guarantees every row from your primary table appears in the result. What SQL puts in the other table's columns when there is no match is where this join type gets interesting.
Lisa Park flagged a pipeline report. Six customers entered the system. Six rows came out. But three rows are half-empty: names and signup dates intact, order columns blank. No WHERE clause. No filter. Nothing was deleted. How do six rows survive a query with half their data missing?
Two tables. Six customers. Three orders.
| id | name | signup |
|---|---|---|
| 1 | Wu | Nov 15 |
| 2 | Patel | Dec 01 |
| 3 | Osei | Jan 10 |
| 4 | Ruiz | Jan 18 |
| 5 | Kim | Oct 05 |
| 6 | Tanaka | Jan 22 |
| order_id | cust_id | amount |
|---|---|---|
| ORD-441 | 1 | $2,400 |
| ORD-445 | 2 | $1,800 |
| ORD-447 | 5 | $3,100 |
| cust_id | name |
|---|---|
| C1 | Alice |
| C2 | Bob |
| C3 | Carol |
| C4 | Dave |
| order_id | cust_id | total |
|---|---|---|
| O1 | C1 | $50 |
| O2 | C1 | $30 |
| O3 | C3 | $75 |
With INNER JOIN, Bob (C2) and Dave (C4) disappear from results because they have no orders. If you're analyzing "all customers," this silently loses data.
LEFT JOIN preserves all rows from your primary table, filling in NULLs where no match exists in the secondary table.
LEFT JOIN (or LEFT OUTER JOIN) keeps ALL rows from the left table. If a row has no match in the right table, the right table's columns are filled with NULL.
Syntax
Watch how every row from the left table appears in the result. Rows without a matching right-side record receive NULL for those columns.
| cust_id | name |
|---|---|
| C1 | Alice |
| C2 | Bob |
| C3 | Carol |
| C4 | Dave |
| order_id | cust_id |
|---|---|
| O1 | C1 |
| O2 | C1 |
| O3 | C3 |
| col_0 | col_1 | col_2 | |
|---|---|---|---|
| C1 | Alice | O1 | C1 |
Notice: Bob and Dave appear in the results even though they have no orders. Their order columns show NULL.
Practical Applications
LEFT JOIN shines when you need complete visibility into one table while optionally pulling in related data from another.
Common Use Cases
> Find customers who have never placed an order.
SELECT c.first_name FROM customers AS c transactions AS t ON c.customer_id = t.user_id WHERE t.transaction_id
LEFT JOIN is the single most commonly used outer join in practice because the need to preserve a primary entity while optionally pulling in related data appears in nearly every reporting scenario.
The NULL values that LEFT JOIN produces for unmatched rows are not a defect but a deliberate signal that tells you exactly which rows had no corresponding record in the joined table.
When you see unexpected NULLs in a LEFT JOIN result, the first thing to check is whether your join condition is correctly matching the foreign key from one table to the primary key of the other.
Finding unmatched rows
Detect missing or orphaned records
When a LEFT JOIN produces NULL values in right-table columns, those NULLs tell you something important: no match was found. This becomes a powerful filter technique.
A powerful pattern: Use LEFT JOIN with WHERE to find rows that have NO match:
This finds customers with zero orders. The WHERE clause filters to only rows where the right table had no match (NULL).
Testing Your Understanding
Practice writing LEFT JOINs and using the NULL pattern to find unmatched rows.
> Complete this query to show all customers alongside their orders, even if a customer has no orders yet.
SELECT c.name, o.total FROM customers AS c orders AS o c.id = o.customer_id
The pattern of LEFT JOIN combined with WHERE on the right-table key being IS NULL is one of the most useful data quality patterns in SQL because it exposes orphaned or incomplete records in a single query.
Once you recognize that NULLs from a LEFT JOIN represent the absence of a match rather than missing data, you can use them as a precise filter to surface exactly the gaps you need to investigate.
WHERE vs ON clause filtering
Control when filters apply in joins
Where you place filter conditions in a LEFT JOIN query dramatically affects which rows survive. Understanding execution order explains why.
One of the most common mistakes with LEFT JOIN is accidentally converting it into an INNER JOIN by filtering on the right table in a WHERE clause. Understanding why this happens requires knowing the order of operations.
Query Evaluation
The database processes your query in a specific order. For a LEFT JOIN query, the evaluation happens in these phases:
The critical insight: WHERE is evaluated AFTER the join is complete. This means WHERE filters the already-joined result set, including the NULL-filled rows from unmatched left table entries.
WHERE on Right Columns
WHERE clause on the right table column removes NULL rows, effectively converting the LEFT JOIN to an INNER JOIN!- Start with customers (Alice, Bob, Carol)
LEFT JOINwith orders on cust_id- Bob has no orders, so his row gets
NULLfor o.total WHEREo.total > 50 filters: Alice (75) kept, Bob (NULL) removed, Carol (30) removed
This behavior stems from how SQL handles NULL in comparisons.
Filter Placement Effects
Moving filters between ON and WHERE clauses changes when conditions are evaluated and which rows survive the query.
Filter in ON Clause
Moving the filter condition into the ON clause changes when the filter is applied:
Comparing the Results
With WHERE o.total > 50: Only Alice appears (1 row). Bob and Carol are filtered out because their rows have NULL or values that don't match.
With the filter in ON: All 3 customers appear. Alice shows her total (75), while Bob and Carol show NULL because they don't have matching high-value orders.
Choosing Right Placement
Both ON and WHERE placements have valid use cases. The choice depends on whether you want to preserve unmatched rows.
When to Use WHERE vs ON
- Applied during the join process
- Unmatched left rows still appear (with NULLs)
- Use when: "Show all X, with Y data if it meets criteria"
- Applied after the join completes
- Removes rows that don't satisfy condition
- Use when: "Only show X that have Y meeting criteria"
Real-World Example
By putting the date filter in the ON clause, customers without recent orders appear with NULL order data instead of being excluded entirely.
ON when you want to preserve all left rows for "optional matching." If you filter on right-table columns in WHERE instead, rows without a match get removed entirely. Remember that NULL comparisons return NULL, not FALSE.> Show all customers with their high-value orders (over $100), keeping customers without such orders.
SELECT c.name, o.total FROM customers AS c LEFT JOIN orders AS o c.id = o.customer_id o.total > 100
The WHERE vs ON distinction for LEFT JOIN filters is one of the most subtle and consequential decisions in SQL, because the results can look superficially similar while being logically very different.
When a colleague asks why their LEFT JOIN is behaving like an INNER JOIN, the WHERE clause filter on a right-table column is almost always the cause, making this one of the most important debugging patterns to recognize.
A reliable mental model is that the ON clause defines which rows can pair together, while the WHERE clause decides which pairs to keep after all rows have been preserved or filled with NULL.
RIGHT JOIN & FULL OUTER
Preserve rows from either or both tables
RIGHT JOIN preserves all rows from the right table instead of the left. While rarely used in practice, understanding it completes the join picture.
RIGHT JOIN is the mirror of LEFT JOIN. It keeps ALL rows from the right table, filling NULL for unmatched left table columns.
Syntax
Notice how every right-side row appears regardless of whether a matching left row exists. Absent left-side values become NULL.
| cust_id | name |
|---|---|
| C1 | Alice |
| C3 | Carol |
| order_id | cust_id | total |
|---|---|---|
| O1 | C1 | $50 |
| O2 | C3 | $30 |
| O3 | C5 | $99 |
| col_0 | col_1 | col_2 | ||
|---|---|---|---|---|
| C1 | Alice | O1 | C1 | $50 |
Notice that the result preserves every order row. When a customer has no matching record (C5), the name column fills with NULL rather than dropping the row entirely.
Invalid References
Order O3 appears with NULL name because it references a customer that doesn't exist. RIGHT JOIN ensures all orders are shown.
RIGHT JOIN is rarely used in practice. Most queries can be rewritten using LEFT JOIN by swapping the table order. Many SQL style guides recommend using LEFT JOIN exclusively for consistency.RIGHT JOIN vs LEFT JOIN
FULL OUTER JOIN Essentials
FULL OUTER JOIN preserves all rows from both tables, making it ideal for data reconciliation and comparison tasks.
FULL OUTER JOIN
FULL OUTER JOIN combines LEFT and RIGHT JOIN. It keeps ALL rows from BOTH tables, filling NULL where there's no match on either side.
Syntax
Observe how neither table loses a row. Both sides are fully preserved, with NULL standing in wherever a match is absent.
| user_id | name |
|---|---|
| U1 | Alice |
| U2 | Bob |
| U3 | Carol |
| user_id | |
|---|---|
| U1 | alice@co |
| U3 | carol@co |
| U4 | dan@co |
| col_0 | col_1 | col_2 | col_3 |
|---|---|---|---|
| U1 | Alice | U1 | alice@co |
Both tables are fully represented in the output. Rows unique to users get NULL in the right columns, rows unique to user_sessions get NULL in the left columns, and matched rows appear complete.
Comparing Two Lists
FULL OUTER JOIN is perfect for reconciliation - comparing two lists to find discrepancies:
| user_id | name |
|---|---|
| U1 | Alice |
| U2 | Bob |
| U3 | Carol |
| user_id | |
|---|---|
| U1 | alice@co.com |
| U3 | carol@co.com |
| U4 | dan@co.com |
> Compare user presence between transactions and push notifications to find all discrepancies.
SELECT t.user_id, p.user_id FROM transactions AS t push_notifs AS p t.user_id = p.user_id
FULL OUTER JOIN is the most comprehensive join type because it guarantees that no row from either table will be dropped, making it ideal for any scenario where you need a complete picture of both datasets.
Data reconciliation between two systems is the most common real-world use case for FULL OUTER JOIN because it lets you identify records that exist in one system but not the other in a single pass.
RIGHT JOIN can almost always be rewritten as a LEFT JOIN by swapping the table order, which is why most teams adopt a convention of using only LEFT JOIN to keep their SQL consistent and easier to read.
Multiple JOINs
Chain three or more tables together
Choosing the Right Join
| Join Type | Preserves | Fills NULL |
|---|---|---|
| INNER | Only matching | Never |
| LEFT | All left rows | Right columns |
| RIGHT | All right rows | Left columns |
| FULL OUTER | All rows both | Both sides |
Relational Context
For an in-depth exploration of table relationships and cardinality notation, see the Relationships lesson in Data Modeling.
In a 1:N relationship from customers to orders, LEFT JOIN from customers shows all customers. Some may have zero orders (NULL in order columns), some may have multiple.
Real-world databases normalize data across multiple tables. To reassemble complete information, you often need to chain multiple JOINs together, connecting tables through their foreign key relationships.
Why Multiple Joins
| cust_id | name |
|---|---|
| C1 | Alice |
| C2 | Bob |
| order_id | cust_id | prod_id |
|---|---|---|
| O1 | C1 | P1 |
| O2 | C1 | P2 |
| O3 | C2 | P1 |
| prod_id | name |
|---|---|
| P1 | Widget |
| P2 | Gadget |
| P3 | Gizmo |
| col_0 | col_1 | col_2 | ||
|---|---|---|---|---|
| C1 | Alice | O1 | C1 | P1 |
Understanding the Query
Database Evaluation
When the database processes multiple JOINs, it evaluates them sequentially from left to right:
- Start with the
FROMtable (customers) - Execute first
JOINto pair customers with orders on cust_id - This creates an intermediate result set
- Execute second
JOINto pair intermediate with products on prod_id - Apply
SELECTto extract the requested columns
Conceptually, each JOIN produces a new virtual table that becomes the input for the next JOIN. The first JOIN combines customers and orders, then that combined result joins with products.
Performance Considerations
Computational Cost
Join Order
While the optimizer rearranges INNER JOINs for performance, your written order matters for readability. Follow this pattern for clear, maintainable queries:
- Start from the "main" entity you're reporting on
- Add related tables one at a time
- Use short, consistent aliases (c, o, p)
- Format each JOIN on its own line
- Chain more than 4-5 joins without testing intermediate results
- Mix alias styles within a single query
Advanced Join Patterns
Mixing Join Types
This keeps all orders (LEFT JOIN to customers), but only orders with valid products (INNER JOIN to products).
> Show all orders with customer names (even orphaned orders), but only for valid products.
SELECT c.name, o.order_id, p.name FROM orders AS o customers AS c o.cust_id = c.id products AS p o.prod_id = p.id
Alias Pitfalls
- Short, meaningful aliases (c, o, p)
- Every column qualified with alias
- Consistent style across the query
- Mixing aliased and non-aliased refs
- Reusing the same alias letter
- Leaving columns ambiguous
> You are a data analyst at DoorDash reconciling restaurants, menu items, and order data to produce a completeness report for the operations team. Relationships between the three tables are often incomplete, and missing data must be surfaced rather than silently dropped.
LEFT JOIN on restaurants to orders preserves every restaurant row, filling order columns with NULL when no orders exist yet.WHERE o.order_id IS NULL after the LEFT JOIN identifies restaurants that have never received a single order.ON clause instead of WHERE preserves the outer join, keeping all restaurants in the result.JOINs chain restaurants to orders then orders to menu_items, with each join result feeding the next stage.LEFT JOIN keeps all left table rows; unmatched rows get NULL for right columnsRIGHT JOIN is the mirror of LEFT JOIN; most prefer LEFT JOIN for consistencyFULL OUTER JOIN keeps all rows from both tables; perfect for data reconciliationWHERE on right-table columns filters out NULLs, converting LEFT to INNER JOINON clause to preserve outer join behaviorLEFT JOIN with a WHERE ... IS NULL check on the right key to find rows with no matchKeeping rows even without a match
- Category
- SQL
- Difficulty
- intermediate
- Duration
- 33 minutes
- Challenges
- 0 hands-on challenges
Topics covered: LEFT JOIN, Finding unmatched rows, WHERE vs ON clause filtering, RIGHT JOIN & FULL OUTER, Multiple JOINs
Lesson Sections
- LEFT JOIN (concepts: sqlLeftJoin)
Consider a common scenario: You want to analyze customer activity, including customers who haven't ordered yet. Here's the problem: Syntax Practical Applications Common Use Cases
- Finding unmatched rows
Testing Your Understanding This technique is particularly valuable during data migrations, integration testing, and routine audits where you need to confirm that every record in one table has a corresponding record in another.
- WHERE vs ON clause filtering
Query Evaluation WHERE on Right Columns Consider this query that tries to find customers with high-value orders: Let's trace through what happens step by step: Filter Placement Effects Filter in ON Clause Now the evaluation changes dramatically: Comparing the Results Choosing Right Placement When to Use WHERE vs ON Both placements have valid use cases - it depends on your intent: Real-World Example Show all customers with their recent orders (last 30 days), including customers who haven't ordere
- RIGHT JOIN & FULL OUTER (concepts: sqlFullOuterJoin)
Syntax Invalid References Consider a scenario where some orders reference customers that don't exist (orphaned data): RIGHT JOIN vs LEFT JOIN These two queries produce identical results: FULL OUTER JOIN Essentials FULL OUTER JOIN Syntax Comparing Two Lists The result shows: Bob exists only in users. Dan exists only in user_sessions. Alice and Carol exist in both. This is data reconciliation in action.
- Multiple JOINs (concepts: sqlMultipleJoins)
Each join type has specific data preservation characteristics. Understanding these helps you select the right tool for your analysis. Choosing the Right Join Each join type preserves different rows. Choose based on which data you can't afford to lose: Relational Context For an in-depth exploration of table relationships and cardinality notation, see the Relationships lesson in Data Modeling. The type of relationship between tables often guides your join choice: Why Multiple Joins Consider an e-c