What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →INNER, LEFT, RIGHT and FULL joins, the anti-join trick for "find rows with no match", and why joins sometimes multiply your rows. Clear examples you can run against any SQL database.
Joins are where SQL starts to feel powerful, and also where many developers start to feel lost. The good news is that you only need a handful of join types for nearly everything, and they all follow one idea: combine rows from two tables where a condition matches.
We'll use two small tables throughout:
users
| id | name |
|---|---|
| 1 | Asha |
| 2 | Ravi |
| 3 | Meera |
orders
| id | user_id | total |
|---|---|---|
| 10 | 1 | 40 |
| 11 | 1 | 25 |
| 12 | 2 | 60 |
| 13 | 9 | 15 |
Meera has no orders. Order 13 belongs to a user (9) who doesn't exist, perhaps a deleted account.
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON o.user_id = u.id;
| name | total |
|---|---|
| Asha | 40 |
| Asha | 25 |
| Ravi | 60 |
Only rows that match on both sides. Meera disappears (no orders) and order 13 disappears (no user). JOIN on its own means INNER JOIN.
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
| name | total |
|---|---|
| Asha | 40 |
| Asha | 25 |
| Ravi | 60 |
| Meera | NULL |
Every user appears. Where there's no matching order, the order columns are NULL. This is the join you'll use most after INNER, for things like "list all customers and their order counts, including those with zero."
NULLs where there's no match. Here it would include Meera and order 13. Useful for reconciling two data sets. (MySQL doesn't support it directly; you'd combine a LEFT and RIGHT join with UNION.)One of the most useful patterns: which users have never ordered?
SELECT u.name
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
Result: Meera. Or, often clearer:
SELECT u.name
FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
The same pattern finds orphaned records too: orders whose user no longer exists. (Foreign key constraints prevent those in the first place.)
Joining a one-to-many relationship repeats the "one" side for each match. Asha appears twice above because she has two orders. That's correct, but it surprises people when they then SUM something from the users table.
Joining two one-to-many relationships at once (users → orders and users → addresses) multiplies rows: two orders × three addresses = six rows for Asha. Totals computed over that result are wrong.
Fixes:
WITH order_totals AS (
SELECT user_id, COUNT(*) AS orders, SUM(total) AS spent
FROM orders
GROUP BY user_id
)
SELECT u.name, COALESCE(t.orders, 0) AS orders, COALESCE(t.spent, 0) AS spent
FROM users u
LEFT JOIN order_totals t ON t.user_id = u.id;
With a LEFT JOIN, where you put a filter changes the result:
-- Keeps all users; only joins orders over 30
LEFT JOIN orders o ON o.user_id = u.id AND o.total > 30
-- Filters AFTER the join: users without big orders disappear,
-- effectively turning it back into an INNER JOIN
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.total > 30
This is one of the most common join bugs. Conditions about the joined table go in ON if you want to keep unmatched left rows.
A table can join to itself, which is useful for hierarchies:
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;
INNER JOIN returns only rows that match in both tables. LEFT JOIN returns every row from the left table, with NULLs where the right table has no match.
Use a LEFT JOIN with WHERE right_table.id IS NULL, or a NOT EXISTS subquery. Both are called anti-joins.
In a one-to-many relationship, the "one" side repeats for every match on the "many" side. Aggregate in a subquery before joining, or select distinct values if that's what you need.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →