Last 30 Days
No notifications
A JOIN combines rows from two or more tables based on a related column. It's the single most important SQL feature beyond SELECT β every report, dashboard and feed query is built from joins. Master INNER, LEFT, RIGHT, FULL, CROSS, self-joins, and the anti-join pattern.
Relational databases split data into many small tables to avoid duplication (normalization). To answer real questions you have to stitch them back together:
users posts
id name id user_id
title
1 Ada 1 1
Hello
2 Linus 2 1
World
3 Grace 3 2
Hi"Show every post with its author's name" requires combining posts and users on posts.user_id = users.id.
SELECT u.name, p.title
FROM users u
INNER JOIN posts p ON p.user_id = u.id;Result: only users with posts (Ada, Linus). Grace has no posts β dropped.
INNER is the default β you can write just JOIN. Aliases (u, p) are universal practice.
SELECT u.name, p.title
FROM users u
LEFT JOIN posts p ON p.user_id = u.id;Now Grace also appears, with p.title = NULL because she has no matches. This is the everyday tool for "show me everyone, plus their X if they have it".
name | title
Ada | Hello
Ada | World
Linus | Hi
Grace | NULL β still shownSELECT u.name, p.title
FROM users u
RIGHT JOIN posts p ON p.user_id = u.id;Keeps every posts row, even orphans whose user_id doesn't match a user. In practice almost everyone uses LEFT JOIN and just swaps the table order β it's easier to read.
SELECT u.name, p.title
FROM users u
FULL OUTER JOIN posts p ON p.user_id = u.id;Returns matched rows + unmatched users + unmatched posts. NULLs fill in where there's no match. (MySQL doesn't support FULL OUTER directly β emulate with LEFT JOIN UNION RIGHT JOIN.)
SELECT u.name, t.tag
FROM users u
CROSS JOIN tags t;Every user paired with every tag. m Γ n rows. Useful for generating combinations (calendar matrices, all-pairs reports). Don't do it accidentally on big tables.
A table joined to itself β for hierarchies, "find pairs", and comparing rows:
-- Find employees and their managers (both rows live in 'employees')
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;Aliases are mandatory here β the engine can't tell which copy you mean otherwise.
If the join columns share the same name, USING is shorter:
SELECT * FROM orders JOIN order_items USING (order_id);
-- equivalent to:
SELECT * FROM orders o JOIN order_items i ON i.order_id = o.order_id;USING (col) collapses the duplicate column in the result. ON keeps both.
SELECT * FROM users NATURAL JOIN posts;Joins on every column with the same name. Brittle β adding a column anywhere can silently change semantics. Don't use it.
-- All users with no posts
SELECT u.*
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE p.id IS NULL;The WHERE p.id IS NULL filter, applied after a LEFT JOIN, keeps only the unmatched rows. Equivalent and often clearer:
SELECT * FROM users u
WHERE NOT EXISTS (SELECT 1 FROM posts p WHERE p.user_id = u.id);SELECT u.name, p.title, c.text AS comment
FROM users u
JOIN posts p ON p.user_id = u.id
LEFT JOIN comments c ON c.post_id = p.id
WHERE u.country = 'IN'
ORDER BY u.name;Joins chain naturally. Each ON clause references the tables already in scope.
For INNER JOIN it usually doesn't matter β both filter rows. For LEFT JOIN it matters a lot:
-- β
Keep all users; only count published posts
SELECT u.name, COUNT(p.id) AS posts
FROM users u
LEFT JOIN posts p ON p.user_id = u.id AND p.status = 'published'
GROUP BY u.name;-- β Drops users with no published posts
SELECT u.name, COUNT(p.id) AS posts
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE p.status = 'published' -- nullifies the LEFT JOIN
GROUP BY u.name;
Rule of thumb: filters on the right side of a LEFT JOIN belong in ON, not WHERE.
| You want⦠| Use |
| Only matched rows | INNER JOIN |
| All left rows + matches if any | LEFT JOIN |
| All rows from both sides | FULL OUTER JOIN |
| All combinations | CROSS JOIN |
| Rows with no match | LEFT JOIN β¦ WHERE right.id IS NULL |
| Hierarchy / pairs in one table | self-join |