Intro to Git — Snapshots, Branches & Sharing
WorkshopyOfficial10 stepsQuiz9 interactiveDiagram~65 min
gitversion controlbeginners
SQL joins exercises on two tables you can see: INNER, LEFT, RIGHT, FULL and CROSS joins animated, with queries you run and answers you check.

WorkshopyOfficial
31 lessons · used by 2 teachers
Sign in to copy it to your workspace. It’s free.
10 steps · 2 quizzes · about 60 minutes · open any step, or read it here
In this lesson you practise SQL joins on two small tables: customers and the orders they placed. You will see which rows each join keeps, which it leaves out, and why, then write joins of your own and check the answers.
Here is the whole idea in one picture. Each line is a pair of rows that match: an order and the customer it belongs to. Press the join types and watch which rows make it into the result.
Interactive · Which rows meet in each join — try it in the lesson
customers.id and orders.customer_idAbout 60 minutes, with checks along the way, a practice set and a final quiz.
Real databases keep each fact once. A customer's name and city live in customers; each purchase lives in orders. The two are connected by a number:
customers.id is the primary key: every customer has a different id.orders.customer_id is a foreign key: it holds the id of the customer who placed the order.So order 101 with customer_id 1 belongs to customer 1, Ava. Matching colours in the tables below show the same key.
Try it: tap Ava, then Dana, then order 105.
Interactive · Customers, orders and the key between them — try it in the lesson
Two rows have no partner, and they will matter all lesson:
customer_id 7, and there is no customer 7. Maybe the account was deleted. Real data has rows like this.Takeaway: a join pairs rows whose keys are equal. Rows with no partner are where the join types differ.
INNER JOIN returns one row for every pair that matches the ON condition, and nothing else.
SELECT c.name, o.id, o.amount
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
Read it in order:
FROM customers c: start from customers, and call it c for short.INNER JOIN orders o: bring in orders, called o.ON c.id = o.customer_id: a customer and an order belong together when these are equal.SELECT c.name, o.id, o.amount: the columns to show from the pairs.Try it: press Run and watch the lines. Then change o.amount to c.city and run it again.
Interactive · INNER JOIN playground — try it in the lesson
Ava appears twice because she placed two orders: one row per matching pair. Dana, Eli and order 105 have no partner, so INNER JOIN leaves them out. JOIN on its own means INNER JOIN.
Check your work · Add `WHERE o.amount > 100` after the ON line and press **Run**. How many rows come back? Paste the number. — check it in the lesson
Takeaway: INNER JOIN keeps matching pairs only; a row that matches twice appears twice.
When two tables are in a query, SQL needs to know which table each column comes from. Both tables have a column called id, so SELECT id is ambiguous and the database refuses it.
table.column: customers.name, orders.id.customers c, and write c.name. Once a table has an alias, use the alias: customers.name no longer works in PostgreSQL.c.* means every column of customers.Try it: press the three buttons under the query and read each message. Then fix the query so it shows each order's id, amount and the customer's city.
Interactive · Which id did you mean? — try it in the lesson
Takeaway: prefix every column in a join with its table's alias. It costs two characters and stops a whole class of mistakes.
Sometimes the rows with no match are the point. A list of all customers with their orders must still show Dana and Eli.
LEFT JOIN keeps every row of the table on its left (the one after FROM). When a left row has no match, its columns from the right table come back as NULL, "no value".
SELECT c.name, o.id, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
Try it: switch between INNER and LEFT and compare the row counts. Then swap the two tables (FROM orders o LEFT JOIN customers c …) and run it: which row is kept with NULLs now?
Interactive · LEFT JOIN playground — try it in the lesson
Left means "the table written first". Swap the tables and you keep every order instead: order 105 comes back with a NULL name, and Dana and Eli are gone.
Takeaway: LEFT JOIN = every row from the left table, plus its matches, NULLs where there are none. It is the join you will use most after INNER.
Use the customers and orders tables from the lesson.
Question 1. What does INNER JOIN return?
Question 2. Dana has no orders. In `customers c LEFT JOIN orders o ON c.id = o.customer_id`, what happens to her?
Question 3. Which column connects an order to its customer?
Question 4. Ava placed two orders. How many rows does Ava get in the INNER JOIN?
Two more joins keep unmatched rows:
| Join | Keeps unmatched rows from | Rows here |
|---|---|---|
| INNER JOIN | neither table | 5 |
| LEFT JOIN | the left table (customers) | 7 |
| RIGHT JOIN | the right table (orders) | 6 |
| FULL OUTER JOIN | both tables | 8 |
RIGHT JOIN is a LEFT JOIN read from the other side: customers c RIGHT JOIN orders o gives the same rows as orders o LEFT JOIN customers c. Most teams write LEFT JOIN and put the table they want to keep first, so queries read the same way.
FULL OUTER JOIN keeps everything: every match, plus Dana and Eli with NULL orders, plus order 105 with a NULL customer. It is how you compare two lists and see what is missing from either side. MySQL has no FULL OUTER JOIN; there you combine a LEFT and a RIGHT join with UNION.
Try it: press each join type and predict the row count before you look.
Interactive · Four joins, one pair of tables — try it in the lesson
A note on the Venn diagrams you may have seen for joins: they show which rows are kept, but not that one customer can match many orders. Ava's two rows are why counting the circles doesn't give the row count.
Takeaway: INNER keeps matches, LEFT and RIGHT also keep one side's leftovers, FULL keeps both sides' leftovers.
The rows with NULLs answer real questions: which customers never ordered? Which orders point to a customer who doesn't exist?
Do a LEFT JOIN, then keep only the rows where the right side found nothing:
SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;
Use IS NULL, never = NULL. NULL means "unknown", and comparing anything with unknown gives unknown, so WHERE o.id = NULL returns no rows at all.
The trap: WHERE after a LEFT JOIN. WHERE runs after the join. A test on a right-table column, like WHERE o.amount > 100, is unknown for the NULL rows, so it throws away the very rows LEFT JOIN kept, and you are back to an inner join. To filter the orders but keep every customer, put the test in ON.
Try it: run the four buttons and compare. Which ones still show Dana and Eli?
Interactive · Find what's missing — try it in the lesson
Takeaway: LEFT JOIN + WHERE right.id IS NULL finds what is missing. Conditions on the right table go in ON when you want to keep every left row.
Before you trust a report, know how many rows the join should give.
CROSS JOIN pairs every row with every row. 5 customers × 6 orders = 30 rows. A join whose condition is wrong behaves the same way and can turn a small table into millions of rows.Try it: run each button and read the row count. Then write SELECT COUNT(*) FROM customers c CROSS JOIN orders o and run it.
Interactive · Count the rows — try it in the lesson
A quick check after any join: count the rows, and ask whether the number makes sense for the question.
Takeaway: a join can only multiply rows through matches. If a count is bigger than you expected, look at the ON condition first.
Joins and GROUP BY work together: join first, then fold the rows into one per group.
SELECT c.name, COUNT(o.id) AS orders, SUM(o.amount) AS total
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.name
ORDER BY total DESC;
Two details make this right:
COUNT(*). COUNT(*) counts rows, and Dana's NULL-filled row is a row, so she would get 1 order. COUNT(o.id) counts non-NULL values, so she gets 0.Dana's total is NULL: there is nothing to add up. In a real report you would write COALESCE(SUM(o.amount), 0) to show 0.
Try it: run the query, then press COUNT(*) instead and see which customers change.
Interactive · Join, then GROUP BY — try it in the lesson
Check your work · Run the query. What is Chen's total? Paste the number. — check it in the lesson
Takeaway: after an outer join, count a column from the right table, never *.
Work out each answer, then check it. Use the playground in the previous step to try a query, or reason from the tables:
| customers.id | name | city |
|---|---|---|
| 1 | Ava | Lisbon |
| 2 | Ben | Austin |
| 3 | Chen | Lisbon |
| 4 | Dana | Oslo |
| 5 | Eli | Austin |
| orders.id | customer_id | amount |
|---|---|---|
| 101 | 1 | 120 |
| 102 | 3 | 80 |
| 103 | 1 | 45 |
| 104 | 2 | 200 |
| 105 | 7 | 60 |
| 106 | 3 | 150 |
customers c INNER JOIN orders o ON c.id = o.customer_id return?customers c LEFT JOIN orders o ON c.id = o.customer_id return?LEFT JOIN orders o ON c.id = o.customer_id WHERE o.id IS NULL: which customers come back?customers c LEFT JOIN orders o … GROUP BY c.city, what does COUNT(o.id) show for Austin?Use the customers and orders tables from the lesson.
Question 1. Which query lists customers who have never placed an order?
Question 2. How many rows does a FULL OUTER JOIN of customers and orders on c.id = o.customer_id return?
Question 3. With customers written first, which joins keep Dana's row? (pick all that apply)
Question 4. A join of a 5-row table and a 6-row table with no ON condition (CROSS JOIN) returns how many rows?
Question 5. You write `LEFT JOIN orders o ON c.id = o.customer_id WHERE o.amount > 100`. What happens to Dana?
Question 6. Why count `COUNT(o.id)` instead of `COUNT(*)` after a LEFT JOIN?
Workshopy shares this lesson under CC BY 4.0. Anyone may use and change it, with credit to you. Anyone can learn it here, free. A copy in your workspace is yours to change and run live; your students never need an account.
Version 1Current
· First published
Copies are made from the current version. A copy you already have doesn’t change.
WorkshopyOfficial10 stepsQuiz9 interactiveDiagram~65 min
gitversion controlbeginners
WorkshopyOfficial16 steps2 quizzes15 interactiveDiagram~120 min
workshopyproduct touronboarding
WorkshopyOfficial9 steps2 quizzes4 interactive~60 min
geometryperimeterarea
WorkshopyOfficial10 steps2 quizzes9 interactive~70 min
sqldatabasesdata analysis
WorkshopyOfficial12 steps2 quizzes12 interactive~85 min
data literacystatisticscharts
WorkshopyOfficial9 steps2 quizzes9 interactive~60 min
machine learningaidata science
Comments 0
No comments yet. Be the first to say how it went.