AiTechWorlds
AiTechWorlds
Imagine you run an online bookstore. You have two spreadsheets: one lists every customer, the other lists every order. A customer is in the first sheet. Their orders are in the second. Neither sheet alone can answer the question: "Which customers placed an order in the last 30 days?"
To answer it, you need to combine the sheets — match each order to the customer who placed it, then filter by date. In SQL, that combining operation is called a JOIN.
Joins are where the relational model pays off. Data stored separately, in normalized tables, gets connected at query time to answer any question the business needs.
customers
| customer_id | name | city |
|---|---|---|
| 1 | Alice | Boston |
| 2 | Ben | Chicago |
| 3 | Clara | Denver |
| 4 | Dan | Boston |
orders
| order_id | customer_id | product | amount |
|---|---|---|---|
| 101 | 1 | SQL Book | 29.99 |
| 102 | 1 | Notebook | 9.99 |
| 103 | 2 | Pen Set | 14.99 |
| 104 | 5 | Eraser | 2.99 |
Notice: Customer 3 (Clara) and 4 (Dan) have no orders. Order 104 has customer_id = 5, which doesn't exist in the customers table (orphaned row). These edge cases reveal the differences between join types.
Returns rows where the join condition is satisfied in both tables. Non-matching rows are excluded entirely.
customers orders
○ ○
○ ←match→ ○ ← INNER JOIN returns these
○ ○
○ (no match) ← excluded
○ (no match) ← excluded
SELECT customers.name, orders.product, orders.amount
FROM customers
INNER JOIN orders ON customers.customer_id = orders.customer_id;
Result:
| name | product | amount |
|---|---|---|
| Alice | SQL Book | 29.99 |
| Alice | Notebook | 9.99 |
| Ben | Pen Set | 14.99 |
Clara and Dan have no orders — they're excluded. Order 104's customer_id doesn't exist — it's excluded. Only rows with a match in both tables appear.
When to use: When you only want records that have a corresponding match. Most common join type.
Returns all rows from the left table plus matching rows from the right table. Where no match exists, right-side columns are NULL.
customers orders
○ ←match→ ○ ← included with order data
○ ←match→ ○ ← included with order data
○ (no match) ← included with NULL order data
○ (no match) ← included with NULL order data
○ (no match) ← excluded (right-only)
SELECT customers.name, orders.product, orders.amount
FROM customers
LEFT JOIN orders ON customers.customer_id = orders.customer_id;
Result:
| name | product | amount |
|---|---|---|
| Alice | SQL Book | 29.99 |
| Alice | Notebook | 9.99 |
| Ben | Pen Set | 14.99 |
| Clara | NULL | NULL |
| Dan | NULL | NULL |
Clara and Dan appear with NULL for order columns — they exist in customers but have no orders. Order 104 still excluded (it's on the right side with no match).
When to use: "Show me all X, and if they have a Y, show that too." Most useful join after INNER JOIN.
Finding rows with no match (anti-join pattern):
-- Customers who have NEVER placed an order
SELECT customers.name
FROM customers
LEFT JOIN orders ON customers.customer_id = orders.customer_id
WHERE orders.order_id IS NULL;
Result:
| name |
|---|
| Clara |
| Dan |
The mirror image of LEFT JOIN. Returns all rows from the right table, plus matching rows from the left.
SELECT customers.name, orders.product, orders.amount
FROM customers
RIGHT JOIN orders ON customers.customer_id = orders.customer_id;
Result:
| name | product | amount |
|---|---|---|
| Alice | SQL Book | 29.99 |
| Alice | Notebook | 9.99 |
| Ben | Pen Set | 14.99 |
| NULL | Eraser | 2.99 |
Order 104 (Eraser) appears with NULL for customer name — it's in orders but has no matching customer.
In practice,
RIGHT JOINis rarely used. You can always rewrite it as aLEFT JOINby swapping the table order. Most developers stick withLEFT JOINfor consistency.
Returns all rows from both tables. Where no match exists on either side, NULLs fill the gaps. Not supported natively in MySQL — emulate with UNION.
-- PostgreSQL / SQL Server
SELECT customers.name, orders.product
FROM customers
FULL OUTER JOIN orders ON customers.customer_id = orders.customer_id;
-- MySQL equivalent
SELECT customers.name, orders.product
FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id
UNION
SELECT customers.name, orders.product
FROM customers RIGHT JOIN orders ON customers.customer_id = orders.customer_id;
Result:
| name | product |
|---|---|
| Alice | SQL Book |
| Alice | Notebook |
| Ben | Pen Set |
| Clara | NULL |
| Dan | NULL |
| NULL | Eraser |
When to use: Auditing data mismatches between two tables. Finding orphans on both sides.
Returns the Cartesian product — every row from the left paired with every row from the right. No ON condition.
SELECT customers.name, orders.product
FROM customers
CROSS JOIN orders;
4 customers × 4 orders = 16 rows. Every possible pairing.
When to use: Generating test data, creating a grid of all possible combinations (e.g., all products for all stores). Almost never used with production data due to explosive row counts.
Used when rows in a table relate to other rows in the same table. Classic example: an employee hierarchy where each employee has a manager_id that references another employee's employee_id.
employees
| employee_id | name | manager_id |
|---|---|---|
| 1 | Sarah | NULL (CEO) |
| 2 | Tom | 1 |
| 3 | Uma | 1 |
| 4 | Victor | 2 |
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
Result:
| employee | manager |
|---|---|
| Sarah | NULL |
| Tom | Sarah |
| Uma | Sarah |
| Victor | Tom |
The table is aliased twice (e for employees, m for managers) so SQL can distinguish which copy is which.
| Join Type | Returns | Right-only rows? | Left-only rows? | When to Use |
|---|---|---|---|---|
INNER JOIN | Matching rows only | No | No | You only want confirmed relationships |
LEFT JOIN | All left + matching right | No | Yes (NULLs) | All left records, optional right |
RIGHT JOIN | All right + matching left | Yes (NULLs) | No | All right records, optional left |
FULL OUTER JOIN | All rows from both | Yes (NULLs) | Yes (NULLs) | Complete picture, find orphans |
CROSS JOIN | All combinations | Yes | Yes | Cartesian product |
SELF JOIN | Any of the above, same table | Varies | Varies | Hierarchies, comparisons within table |
-- This is accidentally a CROSS JOIN in older SQL
SELECT * FROM customers, orders;
-- This is intentionally a CROSS JOIN in modern SQL
SELECT * FROM customers CROSS JOIN orders;
-- This is the INNER JOIN you probably wanted
SELECT * FROM customers
INNER JOIN orders ON customers.customer_id = orders.customer_id;
Without ON, every join between two tables becomes a Cartesian product. With 1,000 customers and 50,000 orders, that's 50 million rows returned.
Joins chain naturally:
SELECT
members.first_name,
books.title,
loans.loan_date,
loans.due_date
FROM loans
INNER JOIN members ON loans.member_id = members.member_id
INNER JOIN books ON loans.book_id = books.book_id
WHERE loans.return_date IS NULL
ORDER BY loans.due_date;
This reads: "Show me the name of every member and the title of every book they currently have on loan, ordered by due date." Three tables, two joins, one readable query.
Next lesson: Advanced queries — aggregation, GROUP BY, subqueries, CTEs, and window functions.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises