By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
(Hyper-Practical, Zero-Fluff Study Guide)
You’re analyzing customer data. One table (customers) has names and emails. Another (orders) has purchase history. You need to answer: - "Which customers placed orders in the last 30 days?" - "Who are our most valuable customers (high spenders)?" - "Are there customers who never placed an order?"
customers
orders
Without joins, you’re stuck exporting CSVs, manually matching IDs in Excel, and praying you didn’t miss a row. Joins let you combine tables on the fly in SQL, saving hours of manual work and reducing errors.
Why this matters in production:- Efficiency: A single JOIN query replaces hours of manual data wrangling.- Accuracy: No more "off-by-one" errors from mismatched Excel rows.- Scalability: Works on millions of rows (unlike Excel, which crashes at 100K+).- Debugging: If your dashboard shows "0 active users" but your boss knows there are 10K, a missing JOIN is often the culprit.
JOIN
Real-world scenario:You inherit a legacy SQL query that calculates "customer lifetime value" (LTV). The query uses INNER JOIN on customers and orders, but your boss asks, "Why are 20% of our customers missing from the report?" Answer: INNER JOIN silently drops customers with no orders. You need LEFT JOIN to include them.
INNER JOIN
LEFT JOIN
LEFT OUTER JOIN
NULL
RIGHT JOIN
RIGHT OUTER JOIN
FULL OUTER JOIN
UNION
LEFT
ON
ON customers.id = orders.customer_id
ON customers.email = orders.email
SELECT DISTINCT
USING
USING (customer_id)
ON customers.customer_id = orders.customer_id
CROSS JOIN
SELF JOIN
employees
FROM employees e1 JOIN employees e2
Goal: Write queries to answer: 1. Which customers placed orders? 2. Which customers never placed orders? 3. What’s the total spend per customer?
Before joining, inspect the tables to avoid surprises:
-- Check customers table SELECT COUNT(*) AS total_customers, COUNT(DISTINCT customer_id) AS unique_customers FROM customers; -- Check orders table SELECT COUNT(*) AS total_orders, COUNT(DISTINCT customer_id) AS customers_with_orders FROM orders;
Expected Output:
total_customers | unique_customers ----------------+----------------- 1000 | 1000 total_orders | customers_with_orders ---------------+---------------------- 5000 | 800
Insight: 200 customers (1000 - 800) have no orders.
SELECT c.customer_id, c.name, c.email, COUNT(o.order_id) AS order_count, SUM(o.amount) AS total_spend FROM customers c INNER JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.name, c.email ORDER BY total_spend DESC;
customer_id | name | email | order_count | total_spend ------------+-----------+---------------------+-------------+------------ 101 | Alice | [email protected] | 5 | 500.00 205 | Bob | [email protected] | 3 | 300.00 ... | ... | ... | ... | ...
Why this works: INNER JOIN only returns customers with at least one order.
SELECT c.customer_id, c.name, c.email, COUNT(o.order_id) AS order_count, COALESCE(SUM(o.amount), 0) AS total_spend FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.name, c.email ORDER BY total_spend DESC;
Key Changes:- LEFT JOIN instead of INNER JOIN.- COALESCE(SUM(o.amount), 0) replaces NULL with 0 for customers with no orders.
COALESCE(SUM(o.amount), 0)
0
customer_id | name | email | order_count | total_spend ------------+-----------+---------------------+-------------+------------ 101 | Alice | [email protected] | 5 | 500.00 205 | Bob | [email protected] | 3 | 300.00 300 | Charlie | [email protected] | 0 | 0.00 ... | ... | ... | ... | ...
Insight: Now you see all customers, including Charlie (who never ordered).
(Not supported in MySQL; use UNION instead.)
-- PostgreSQL/BigQuery/SQLite SELECT c.customer_id, c.name, o.order_id, o.amount FROM customers c FULL OUTER JOIN orders o ON c.customer_id = o.customer_id ORDER BY c.customer_id, o.order_id;
MySQL Workaround:
SELECT c.customer_id, c.name, o.order_id, o.amount FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id UNION SELECT c.customer_id, c.name, o.order_id, o.amount FROM customers c RIGHT JOIN orders o ON c.customer_id = o.customer_id WHERE c.customer_id IS NULL;
customer_id | name | order_id | amount ------------+-----------+----------+-------- 101 | Alice | 1 | 100.00 101 | Alice | 2 | 200.00 NULL | NULL | 3 | 50.00 -- Orphaned order (no customer) 300 | Charlie | NULL | NULL -- Customer with no orders
Insight: Useful for finding data inconsistencies (e.g., orders with no customer).
Mistake: Filtering in the WHERE clause after a LEFT JOIN can turn it into an INNER JOIN:
WHERE
-- ❌ Wrong: This filters out customers with no orders (same as INNER JOIN) SELECT c.customer_id, c.name FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE o.amount > 100;
Fix: Filter in the ON clause:
-- ✅ Correct: Keeps all customers, only joins orders > $100 SELECT c.customer_id, c.name FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id AND o.amount > 100;
customer_id
sql CREATE INDEX idx_orders_customer_id ON orders(customer_id);
SELECT *
EXPLAIN
sql EXPLAIN SELECT * FROM customers c JOIN orders o ON c.customer_id = o.customer_id;
c
o
c.name
name
customers LEFT JOIN orders
SELECT DISTINCT customer_id FROM customers
SELECT DISTINCT customer_id FROM orders
LIMIT 100
COALESCE
ISNULL
COALESCE(total_spend, 0)
sql SELECT COUNT(*) FROM ( SELECT customer_id, COUNT(*) AS cnt FROM joined_data GROUP BY customer_id HAVING COUNT(*) > 1 ) dupes;
GROUP BY
SUM
❌ RIGHT JOIN (all rows from right table)
"You need to find customers who never placed an order. Which join?"
WHERE orders.customer_id IS NULL
❌ INNER JOIN (drops customers with no orders)
"What’s the difference between ON and USING?"
ON a.id = b.id
USING (id)
"You’re analyzing a dataset of employees and their managers. The employees table has employee_id and manager_id (which references employee_id). How do you find all employees and their managers’ names?" Answer:
employee_id
manager_id
SELECT e.employee_id, e.name AS employee_name, m.name AS manager_name FROM employees e LEFT JOIN employees m ON e.manager_id = m.employee_id;
Why: This is a SELF JOIN (joining a table to itself).
Challenge:You have two tables: - products (product_id, product_name, category) - sales (sale_id, product_id, quantity, sale_date)
products
sales
Write a query to: 1. Show all products, including those never sold.2. Calculate total quantity sold per product.3. Order by total quantity (descending).
Solution:
SELECT p.product_id, p.product_name, p.category, COALESCE(SUM(s.quantity), 0) AS total_quantity_sold FROM products p LEFT JOIN sales s ON p.product_id = s.product_id GROUP BY p.product_id, p.product_name, p.category ORDER BY total_quantity_sold DESC;
Why it works: - LEFT JOIN ensures all products are included.- COALESCE replaces NULL with 0 for unsold products.- GROUP BY aggregates sales by product.
FROM a INNER JOIN b ON a.id = b.id
FROM a LEFT JOIN b ON a.id = b.id
FROM a RIGHT JOIN b ON a.id = b.id
FROM a FULL JOIN b ON a.id = b.id
FROM a CROSS JOIN b
Key Commands:
-- Check for NULL after LEFT JOIN SELECT * FROM a LEFT JOIN b ON a.id = b.id WHERE b.id IS NULL; -- MySQL FULL OUTER JOIN workaround SELECT * FROM a LEFT JOIN b ON a.id = b.id UNION SELECT * FROM a RIGHT JOIN b ON a.id = b.id WHERE a.id IS NULL; -- Self join (employees and managers) SELECT e.name, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.employee_id;
⚠️ Exam Traps:- LEFT JOIN + WHERE b.id = X → behaves like INNER JOIN.- FULL OUTER JOIN doesn’t work in MySQL.- USING only works if column names match.
WHERE b.id = X
Final Tip:Joins are like Lego blocks—start with INNER JOIN, then experiment with LEFT JOIN when you need "all rows from X." Always verify your results with COUNT(*) before and after joining.
COUNT(*)
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.