A SQL JOIN combines rows from two or more tables using a column they share, such as a customer ID. Use INNER JOIN when you only want rows that match in both tables, LEFT JOIN when you want every row from the first table even if there is no match, and FULL OUTER JOIN when you want everything from both sides. This guide walks through each type with two tiny tables so you can see exactly which rows come back.
Why databases split data into tables
Relational databases store each kind of thing in its own table: customers in one, orders in another, products in a third. A customer's name and city are stored once, and each order simply points to the customer with a customer_id. This avoids repeating the same information thousands of times and keeps it consistent when something changes.
The price of that design is that most business questions need data from more than one table. "Total sales by customer city" needs the city (in customers) and the amount (in orders). JOINs are how you bring them back together. If you are new to queries, start with our SQL for beginners guide on SELECT, WHERE and ORDER BY.
The two sample tables
customers
| customer_id | name | city |
|---|---|---|
| 1 | Ana Rivera | Miami |
| 2 | Brooks Market | Atlanta |
| 3 | Coastal Deli | Tampa |
| 4 | Delta Foods | Houston |
orders
| order_id | customer_id | order_date | amount |
|---|---|---|---|
| 101 | 1 | 2026-09-02 | 320.00 |
| 102 | 1 | 2026-09-20 | 150.00 |
| 103 | 2 | 2026-09-05 | 1250.00 |
| 104 | 3 | 2026-09-11 | 780.50 |
| 105 | 9 | 2026-09-30 | 60.00 |
Notice two "imperfections" that exist in real data too: Delta Foods (customer 4) has no orders, and order 105 points to customer 9, who doesn't exist in the customers table. These two rows are what make each JOIN type behave differently.
INNER JOIN: only the matches
An INNER JOIN returns rows where the join condition is true on both sides.
SELECT c.name, o.order_id, o.amount
FROM customers AS c
INNER JOIN orders AS o
ON c.customer_id = o.customer_id
ORDER BY o.order_id;
| name | order_id | amount |
|---|---|---|
| Ana Rivera | 101 | 320.00 |
| Ana Rivera | 102 | 150.00 |
| Brooks Market | 103 | 1250.00 |
| Coastal Deli | 104 | 780.50 |
Delta Foods disappears (no orders) and so does order 105 (no matching customer). Ana appears twice because she has two orders: a JOIN returns one row per matching pair. Writing just JOIN is the same as INNER JOIN.
The c and o are table aliases. They keep queries short and make it clear which table each column comes from, which matters when both tables have a column with the same name.
LEFT JOIN: keep everything on the left
A LEFT JOIN (also called LEFT OUTER JOIN) returns every row from the first table, plus matching data from the second. When there is no match, the columns from the second table are NULL.
SELECT c.name, o.order_id, o.amount
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_id;
The result is the same four rows as before plus one more: Delta Foods, NULL, NULL. Order 105 still doesn't appear, because it isn't in the left table.
Finding rows with no match
Combining a LEFT JOIN with IS NULL is one of the most useful patterns in analytics. It answers questions like "which customers have never ordered?"
SELECT c.name
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
Result: Delta Foods. Swap the tables and you can find orders whose customer is missing, a common data-quality check before building a dashboard.
WHERE o.amount > 100 removes the NULL rows and silently turns your LEFT JOIN into an INNER JOIN. If you want to keep all customers, move the condition into the ON clause: ON c.customer_id = o.customer_id AND o.amount > 100.RIGHT, FULL OUTER and CROSS JOIN
| JOIN type | Returns | In our example |
|---|---|---|
| INNER JOIN | Only matching rows | 4 rows |
| LEFT JOIN | All left rows + matches | 5 rows (adds Delta Foods) |
| RIGHT JOIN | All right rows + matches | 5 rows (adds order 105) |
| FULL OUTER JOIN | All rows from both sides | 6 rows (adds both) |
| CROSS JOIN | Every combination | 4 × 5 = 20 rows |
RIGHT JOIN is a mirror of LEFT JOIN; most analysts simply swap the table order and use LEFT JOIN so queries read consistently. FULL OUTER JOIN is great for reconciling two lists, such as orders in the sales system vs invoices in accounting. MySQL doesn't support FULL OUTER JOIN directly (you can emulate it with a LEFT JOIN and a RIGHT JOIN combined with UNION), and SQLite added RIGHT and FULL joins only in version 3.39. CROSS JOIN has no ON clause; it is useful for building grids such as every store × every month.
A real business query: sales by customer
JOINs become powerful when combined with aggregation. This query lists every customer with their order count and total sales, including those with zero orders:
SELECT
c.name,
COUNT(o.order_id) AS orders,
COALESCE(SUM(o.amount), 0) AS total_sales
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
GROUP BY c.name
ORDER BY total_sales DESC;
Result: Brooks Market 1250.00, Coastal Deli 780.50, Ana Rivera 470.00, Delta Foods 0. Note that COUNT(o.order_id) counts only non-null values, so Delta Foods correctly shows 0 orders, while COUNT(*) would show 1. Learn more in our guide to GROUP BY and aggregate functions.
Watch out for duplicated rows
If the join column is not unique on at least one side, rows multiply. Joining orders to a promotions table where a customer has three promotions repeats each order three times, and your sales total triples. Before joining, ask "is this key unique in this table?" and compare row counts before and after the join.
Joining more than two tables
Real reports often chain several JOINs. Imagine two more tables: order_items (one row per product in each order, with order_id, product_id and quantity) and products (with product_id and category). To get units sold by customer city and product category, you join step by step, each JOIN connecting to a table already in the query:
SELECT
c.city,
p.category,
SUM(oi.quantity) AS units
FROM orders AS o
INNER JOIN customers AS c ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi ON o.order_id = oi.order_id
INNER JOIN products AS p ON oi.product_id = p.product_id
GROUP BY c.city, p.category;
Read it as a path: orders lead to customers, orders lead to their items, and items lead to products. Sketching that path on paper before writing the query is a simple habit that prevents most join mistakes.
Next steps
Recreate the two tables in a free database like SQLite or PostgreSQL and run every query above, predicting the row count before you press run. Then practice the LEFT JOIN + IS NULL pattern on your own data. When you're ready, move on to CTEs and subqueries to organize multi-step joins, and to star schema modeling to see how the same relationships work inside Power BI.
Watch: video lessons


Frequently asked questions
What is the difference between INNER JOIN and LEFT JOIN?
INNER JOIN returns only rows that match in both tables. LEFT JOIN returns every row from the left table and fills the right table's columns with NULL when there is no match.
Is JOIN the same as INNER JOIN?
Yes. Writing JOIN without a type is treated as an INNER JOIN in standard SQL and in all major databases.
Why does my JOIN return more rows than my table?
The join key is probably not unique on the other side, so each row matches several rows. Check for duplicate keys and compare row counts before and after joining.
Does MySQL support FULL OUTER JOIN?
Not directly. You can get the same result by combining a LEFT JOIN and a RIGHT JOIN with UNION.
Mark this lesson complete to track your rank progress.