Joins are how SQL truly earns its keep. They let you ask questions that span multiple tables — and most real questions do.
Picture two tables: customers and orders. Each customer has many orders. To get 'each order with the customer's name attached', you JOIN them on customer_id.
INNER JOIN
SELECT o.id, o.total_amount, c.first_name, c.countryFROM orders oINNER JOIN customers c ON o.customer_id = c.id;
Returns one row per order, with the matching customer's columns alongside. The ON clause says 'match orders to customers where the customer_id columns are equal'. Aliases (o, c) are optional but make joins much more readable.
INNER excludes orphans
INNER JOIN drops any orders without a matching customer (and any customers with no orders). If you want to keep one side regardless, use OUTER joins.
LEFT JOIN
SELECT c.id, c.first_name, COUNT(o.id) AS order_countFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id, c.first_name;
Keeps every customer, even those with no orders (their order_count is 0). LEFT JOIN means: keep all rows from the left table; fill matching right-table columns with NULL where there's no match.
RIGHT JOIN and FULL OUTER JOIN
RIGHT JOIN is the mirror of LEFT — keep all of the right table. Most analysts never write RIGHT JOIN; they swap the table order and use LEFT instead because it reads more naturally.
FULL OUTER JOIN keeps every row from both tables, NULL-padding non-matches on either side. Useful for set comparisons (which records are in A but not B?).
Multiple joins
SELECT o.id, c.first_name, p.name AS product_nameFROM orders oJOIN customers c ON c.id = o.customer_idJOIN order_items i ON i.order_id = o.idJOIN products p ON p.id = i.product_id;
Daisy-chain as many JOINs as needed. Each JOIN adds another table and an ON condition. Performance falls off if you have 10+ joins or join two huge tables without indexes — we cover that in module 10.
The cartesian explosion
Forgetting the ON clause causes a CROSS JOIN — every row of A paired with every row of B. With 1,000 customers and 10,000 orders, that's 10 million rows. Always check your row counts after joining.
Exercise
Get a list of customers (first_name, country) and the total amount they've spent. Include customers with no orders.