Posted by Kyle Hankinson May 5, 2021
JOINs combine rows from two or more tables based on a related column. Understanding which rows each join type returns is fundamental to writing correct queries.
-- customers -- orders
-- id | name -- id | customer_id | total
-- 1 | Alice -- 1 | 1 | 50.00
-- 2 | Bob -- 2 | 1 | 75.00
-- 3 | Carol -- 3 | 3 | 30.00
Bob has no orders. Orders all belong to Alice or Carol.
Returns only rows that have a match in both tables:
SELECT c.name, o.total
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
| name | total |
|---|---|
| Alice | 50.00 |
| Alice | 75.00 |
| Carol | 30.00 |
Bob is excluded because he has no matching order.
Returns all rows from the left table, plus matching rows from the right. Non-matching right-side columns are NULL:
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
| name | total |
|---|---|
| Alice | 50.00 |
| Alice | 75.00 |
| Bob | NULL |
| Carol | 30.00 |
Bob appears with NULL because he has no orders. This is the most common join for "show everything, even if there's no match."
The mirror of LEFT JOIN — returns all rows from the right table:
SELECT c.name, o.total
FROM customers c
RIGHT JOIN orders o ON c.id = o.customer_id;
In practice, RIGHT JOIN is rarely used. You can always rewrite it as a LEFT JOIN by swapping the table order.
Returns all rows from both tables, with NULLs where there is no match:
SELECT c.name, o.total
FROM customers c
FULL OUTER JOIN orders o ON c.id = o.customer_id;
This is useful when you need to find unmatched rows on both sides — for example, customers without orders AND orders without valid customers.
MySQL lacks native FULL OUTER JOIN support. The workaround is a UNION of LEFT and RIGHT joins:
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
UNION
SELECT c.name, o.total
FROM customers c
RIGHT JOIN orders o ON c.id = o.customer_id;
Returns every combination of rows from both tables (the Cartesian product):
SELECT c.name, p.product_name
FROM customers c
CROSS JOIN products p;
If customers has 3 rows and products has 10 rows, the result has 30 rows. Useful for generating combinations but dangerous on large tables.
A table joined to itself — commonly used for hierarchical data:
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Forgetting the ON clause turns your join into a CROSS JOIN, producing a massive result set.
Filtering in WHERE vs ON matters for outer joins:
-- This filters AFTER the join, removing NULLs
SELECT * FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.total > 50;
-- This filters DURING the join, keeping all customers
SELECT * FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id AND o.total > 50;
The second query preserves all customers, showing NULL for those without qualifying orders.
About the author — Kyle Hankinson is the founder and sole developer of SQLPro Studio and the Hankinsoft Development suite of database tools. He has been building native macOS and iOS applications since 2010.
Try SQLPro Studio — A powerful database manager for MySQL, PostgreSQL, Microsoft SQL Server, SQLite, Oracle, and Snowflake. Available on macOS, iOS, and Windows.
Download Free Trial View Pricing Compare