SQL & Data

SQL JOINs Explained with Simple Examples

SQL JOINs explained with two small tables: INNER, LEFT, RIGHT, FULL OUTER and CROSS JOIN, finding unmatched rows and avoiding duplicated totals.

By DashCore Hub Editorial TeamUpdated 8 min read

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_idnamecity
1Ana RiveraMiami
2Brooks MarketAtlanta
3Coastal DeliTampa
4Delta FoodsHouston

orders

order_idcustomer_idorder_dateamount
10112026-09-02320.00
10212026-09-20150.00
10322026-09-051250.00
10432026-09-11780.50
10592026-09-3060.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;
nameorder_idamount
Ana Rivera101320.00
Ana Rivera102150.00
Brooks Market1031250.00
Coastal Deli104780.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.

Tip: Be careful when filtering the right-hand table of a LEFT JOIN. A condition like 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 typeReturnsIn our example
INNER JOINOnly matching rows4 rows
LEFT JOINAll left rows + matches5 rows (adds Delta Foods)
RIGHT JOINAll right rows + matches5 rows (adds order 105)
FULL OUTER JOINAll rows from both sides6 rows (adds both)
CROSS JOINEvery combination4 × 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.

AI shortcut: When a JOIN returns surprising numbers, paste the table structures and your query into Claude or ChatGPT: "My orders table has 5,000 rows but this LEFT JOIN returns 7,300. Here are the CREATE TABLE statements and the query. Explain why rows are duplicating and give me a query to find the duplicate keys." The AI is good at spotting non-unique keys and misplaced filters.

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

SQL Joins Tutorial for Beginners - Inner Join, Left Join, Right Join, Full Outer Joinby Joey Blue · Watch on YouTube ↗
Uso de Inner, Left, Right y Full Joinby DevGuardian Code · Watch on YouTube ↗Video in Spanish.

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.

Learning with the Academy?
Mark this lesson complete to track your rank progress.
Take the quiz