Overview
A join combines rows from two tables based on a related column, typically a foreign key. INNER JOIN returns only rows that match in both tables; LEFT JOIN returns every row of the left table with matches or NULLs; RIGHT and FULL OUTER joins are the mirror and the union; CROSS JOIN returns every combination.
Joins are what make normalized schemas practical: data lives in one place, and queries assemble it. At scale, joins are also where performance problems appear, so you should know how join type, indexes on join keys, and row counts affect cost, and when to denormalize instead.
INNER JOIN lists guests who sent an RSVP. LEFT JOIN lists every invited guest, with a blank if they did not reply. FULL OUTER JOIN also includes RSVP cards from people who were never invited.
When to use it
- Combining normalized data (orders with customers).
- Finding missing relationships (customers with no orders).
- Reporting and analytics across entities.
- Deciding when denormalization is justified.
Where it shows up in interviews
Recognize it when: combine entities for a page or report.
- Show a user's orders with product names
- Top customers by revenue
Recognize it when: find records without a related record.
- Users who never purchased
- Products with no reviews
Where it is used in real software
Hibernate, Prisma, and ActiveRecord generate joins; eager loading prevents N+1 query problems.
Star schemas join a large fact table (sales) with dimension tables (date, product, store).
Cross-shard joins are expensive, so sharded designs co-locate related data by the same shard key.
Key terms
- INNER JOIN
- Only matching rows from both sides.
- LEFT JOIN
- All left rows; NULLs where no right match.
- FULL OUTER JOIN
- All rows from both sides.
- Anti-join
- LEFT JOIN ... WHERE right.id IS NULL, or NOT EXISTS.
- Self join
- Joining a table to itself (employee and manager).
How it works, step by step
- 1Identify the relationship
orders.customer_id references customers.id.
- 2Choose the join type
Do you need unmatched rows? LEFT. Only matches? INNER.
- 3Index the join keys
Especially the foreign key side.
- 4Filter early
WHERE clauses reduce rows before expensive joins.
- 5Check row multiplication
Joining one-to-many relationships can duplicate rows; aggregate carefully.
Join types on a small dataset
customers: Ana (1), Bo (2), Cy (3). orders: #10 by 1, #11 by 1, #12 by 4 (unknown customer)
| Join | Rows returned |
|---|---|
| INNER JOIN | Ana-#10, Ana-#11 |
| LEFT JOIN (customers left) | Ana-#10, Ana-#11, Bo-NULL, Cy-NULL |
| RIGHT JOIN (orders right) | Ana-#10, Ana-#11, NULL-#12 |
| FULL OUTER JOIN | Ana-#10, Ana-#11, Bo-NULL, Cy-NULL, NULL-#12 |
| Anti-join (customers without orders) | Bo, Cy |
NOWJoin: INNER JOIN | Rows returned: Ana-#10, Ana-#11
Order #12 references a missing customer, which a foreign key constraint would have prevented.
Implementation
-- Orders with customer names (only matches)SELECT o.id, c.name, o.total_centsFROM orders oJOIN customers c ON c.id = o.customer_id; -- All customers with their order count, including zeroSELECT c.name, count(o.id) AS ordersFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id, c.name; -- Customers who never ordered (anti-join)SELECT c.name FROM customers cWHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id); -- Beware row multiplication: joining items AND payments to orders duplicates rowsSELECT o.id, (SELECT sum(qty) FROM order_items i WHERE i.order_id = o.id) AS items, (SELECT sum(amount) FROM payments p WHERE p.order_id = o.id) AS paidFROM orders o;Complexity and performance
Nested loop with index.
Memory for the smaller side.
Trade-offs
Joins keep one source of truth; denormalizing speeds reads but duplicates data and complicates writes.
Databases join efficiently with indexes; application-side joins across services cost network round trips.
Variants and related techniques
Run a subquery per row, for example 'latest 3 orders per customer'.
EXISTS returns left rows that have at least one match, without duplicates.
Common mistakes
- Filtering the right table in WHERE after a LEFT JOIN.
Fix: WHERE o.status = 'paid' turns it into an inner join; put the condition in the ON clause.
- Row explosion from multiple one-to-many joins.
Fix: Aggregate in subqueries before joining.
- Missing index on the foreign key.
Fix: Index orders.customer_id.
Interview questions
What is the difference between INNER and LEFT JOIN?
INNER returns only rows with matches in both tables. LEFT returns every row from the left table, with NULLs for columns of the right table where no match exists.
How do you avoid cross-shard joins in a sharded database?
Shard related tables by the same key (for example tenant_id or user_id) so joins stay within one shard, and denormalize or precompute data needed across shards.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Write queries for each join type | Easy | Semantics. |
| Latest order per customer | Medium | LATERAL or window functions. |