Real businesses never keep everything in one table. Orders live in an orders table, customer details in a customers table, product attributes in a products table. That design keeps data clean, but it means almost every report you write as an analyst has to stitch tables back together — because a manager wants to see the customer's name and city, not a bare customer_id. Joins are how you do that, and they are among the most-used and most-tested skills in the analyst toolkit.
This tutorial explains the four join types through the lens of reporting on customers and orders. It follows the reporting basics in the SQL for analytics path and pairs naturally with grouping.
Two tables to join
-- customers
customer_id | customer_name | city
------------+---------------+----------
1 | Aarti | Hyderabad
2 | Bhaskar | Hyderabad
3 | Chitra | Pune
4 | Devan | Chennai
-- orders
order_id | customer_id | amount
---------+-------------+-------
1001 | 1 | 1200
1002 | 2 | 8500
1003 | 3 | 1200
1004 | 1 | 600
1005 | 9 | 4000
Notice two things: customer 4 (Devan) has no orders, and order 1005 references customer_id 9, who does not exist in the customers table. These mismatches are exactly where the join types differ.
INNER JOIN: only matched rows
INNER JOIN keeps only rows that have a match on both sides. To report each order with its customer's name:
SELECT o.order_id, c.customer_name, o.amount
FROM orders o
INNER JOIN customers c
ON o.customer_id = c.customer_id;
order_id | customer_name | amount
---------+---------------+-------
1001 | Aarti | 1200
1002 | Bhaskar | 8500
1003 | Chitra | 1200
1004 | Aarti | 600
Order 1005 vanishes because customer_id 9 has no match, and Devan never appears because he placed no orders. INNER JOIN is the right choice when you only want fully matched data. The o and c are table aliases that keep the query short and let you qualify columns that exist in both tables.
LEFT JOIN: keep every left row
LEFT JOIN returns every row from the left (first) table, matched where possible and filled with NULL where not. To list every customer and their orders, including customers with none:
SELECT c.customer_name, o.order_id, o.amount
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id;
customer_name | order_id | amount
--------------+----------+-------
Aarti | 1001 | 1200
Aarti | 1004 | 600
Bhaskar | 1002 | 8500
Chitra | 1003 | 1200
Devan | NULL | NULL
Devan now appears with NULLs, because LEFT JOIN refuses to drop him. This is the single most useful pattern in analytics: it lets you report on the complete set of entities, not just the ones with activity. "Every customer, including those who never ordered" or "every product, including those with zero sales" both require a LEFT JOIN.
A LEFT JOIN also finds the gaps. To list customers with no orders at all:
SELECT c.customer_name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
The WHERE ... IS NULL keeps only the unmatched rows — a standard "who is missing" report.
RIGHT and FULL joins
RIGHT JOIN is a mirror of LEFT JOIN: it keeps every row from the right table. Most analysts avoid it and simply reorder the tables to use LEFT JOIN, which reads more naturally. FULL OUTER JOIN keeps unmatched rows from both sides at once — every customer and every order, with NULLs wherever there is no match. It is useful for reconciliation reports where you want to surface orphans on either side, like our order 1005 pointing at a missing customer.
Joins and grouping together
Joins get their real power when combined with grouping. Total revenue per city needs the customer table for the city and the orders table for the amount:
SELECT c.city,
COUNT(o.order_id) AS orders,
SUM(o.amount) AS revenue
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.city
ORDER BY revenue DESC;
The LEFT JOIN ensures a city with no orders still shows up with zero. This join-then-group shape is the backbone of nearly every dashboard, which is why it connects so tightly to GROUP BY and HAVING.
Common mistakes
- Row fan-out inflating totals. If the join key is not unique on one side, rows multiply and
SUM/COUNToverstate reality. Confirm the key is unique where you assume it is, and sanity-check totals. - Using INNER JOIN when you needed LEFT. An INNER JOIN silently drops entities with no matches, so "customers with zero orders" simply disappear and your report looks complete when it is not.
- Filtering a LEFT JOIN's right table in WHERE. Putting
WHERE o.amount > 1000on a LEFT JOIN turns it back into an inner join by discarding the NULL rows. Put such conditions in theONclause if you want to preserve unmatched left rows. - Ambiguous column names. When both tables share a column name, referencing it unqualified errors out. Always alias tables and prefix shared columns.
In interviews
Joins are the heart of analyst SQL interviews. Expect "customers who never placed an order" (LEFT JOIN plus IS NULL), "total revenue per city" (join plus GROUP BY), and the conceptual "difference between INNER and LEFT JOIN". A favourite trap is asking why a join returned too many rows — the answer is a non-unique key causing fan-out. Being able to explain fan-out and the LEFT-JOIN-then-filter pitfall marks you as someone who has debugged real reports.
Where this fits in your learning path
Joins connect the tables that grouping and aggregating then summarize, so they pair with GROUP BY and HAVING on nearly every dashboard. When a table needs to join to itself — employees to managers, or comparing rows within one table — continue to self joins. Joins are a graded skill throughout the data analyst roadmap.
Frequently Asked Questions
What is the difference between INNER JOIN and LEFT JOIN?
How do I join two tables in SQL?
Why does my join return more rows than expected?
When should I use a RIGHT JOIN?
What is the difference between a join and a UNION?
Want to Build Your Career in Data Analytics with AI?
Join CodeBegun and train with working industry engineers — View the Data Analytics curriculum

