SQL exercises with solutions: JOINs

SQL exercises with solutions: JOINs

Updated on

JOIN links two tables through a shared column; LEFT JOIN also keeps the rows with no match. These 15 exercises range from level 2 to level 8; they use the syntax of SQLite, SpeedQL’s SQL engine.

Tip: Give each table a short alias (c, o) and always write the ON condition.

Read the “JOIN” card in the cheat sheet

Exercise 1 · level 2

Show the name of each customer with the amount of each of their orders.

Table customers (4 rows)
idnamecity
1AliceParis
2BrunoLyon
3ChloeParis
4DylanNantes
Table orders (6 rows)
idcustomer_idamount
11120
2180
32200
4350
5375
6330
Show the hint

Topics to use: JOIN.

Query structure:

SELECT , 
FROM 
INNER JOIN  ON  = 
Show the solution
SELECT customers.name, orders.amount
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id;

Expected result (6 rows):

nameamount
Alice80
Alice120
Bruno200
Chloe30
Chloe50
Chloe75

Exercise 2 · level 2

Show the id of each flight with the city of the departure airport.

Table flights (12 rows)
idairlineorigindestdepartsduration_minpriceseats_soldcapacity
1SkyJetCDGMAD2025-06-01 08:1012589150180
2AirNovaCDGLIS2025-06-01 11:40155120160170
3BlueWingLYSFCO2025-06-02 07:309575110150
4SkyJetMADCDG2025-06-02 18:2012095170180
5AirNovaBERCDG2025-06-03 09:05110105140160
6BlueWingNCEBER2025-06-03 13:5013014090150
7SkyJetCDGFCO2025-06-04 06:4513599175180
8AirNovaLISMAD2025-06-04 16:15756560120
9BlueWingFCOLYS2025-06-05 20:3010082130150
10SkyJetCDGBER2025-06-05 10:00105110120180
11AirNovaMADLIS2025-06-06 12:25807095120
12BlueWingLYSMAD2025-06-06 15:4011579100150
Table airports (7 rows)
codecitycountry
CDGParisFrance
LYSLyonFrance
MADMadridSpain
LISLisbonPortugal
FCORomeItaly
BERBerlinGermany
NCENiceFrance
Show the hint

Topics to use: JOIN.

Query structure:

SELECT , 
FROM  
JOIN   ON  = 
Show the solution
SELECT f.id, a.city
FROM flights f
JOIN airports a ON a.code = f.origin;

Expected result (12 rows):

idcity
1Paris
2Paris
3Lyon
4Madrid
5Berlin
6Nice
7Paris
8Lisbon
9Rome
10Paris
11Madrid
12Lyon

Exercise 3 · level 3

Show each city with the total number of orders placed by the customers of that city.

Table customers (4 rows)
idnamecity
1AliceParis
2BrunoLyon
3ChloeParis
4DylanNantes
Table orders (6 rows)
idcustomer_idamount
11120
2180
32200
4350
5375
6330
Show the hint

Topics to use: COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , COUNT(*)
FROM 
INNER JOIN  ON  = 
GROUP BY 
Show the solution
SELECT customers.city, COUNT(*)
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id
GROUP BY customers.city;

Expected result (2 rows):

cityCOUNT(*)
Lyon1
Paris5

Exercise 4 · level 3

Show the name of each director and their number of movies, including 0 for those who have none.

Table movies (10 rows)
idtitlegenreyeardurationratingdirector_id
1Night TrainThriller20151187.81
2Blue HarborDrama20181027.12
3Paper Moon CityComedy2012956.45
4Silent PeakDrama20201318.23
5Last SignalSci-Fi20191427.51
6Summer KeysComedy2016885.94
7Iron GardenSci-Fi202112583
8Dust and GoldWestern20141106.82
9The Quiet HourDrama2022977.45
10Deep CurrentThriller20171056.9NULL
Table directors (6 rows)
idnamecountry
1Nora EllisUK
2Paulo ReisBrazil
3Kenji MoriJapan
4Anna BergSweden
5Luc MartinFrance
6Sara DiazSpain
Show the hint

Topics to use: COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , COUNT()
FROM  
LEFT JOIN   ON  = 
GROUP BY 
Show the solution
SELECT d.name, COUNT(m.id)
FROM directors d
LEFT JOIN movies m ON m.director_id = d.id
GROUP BY d.id;

Expected result (6 rows):

nameCOUNT(m.id)
Nora Ellis2
Paulo Reis2
Kenji Mori2
Anna Berg1
Luc Martin2
Sara Diaz0

Exercise 5 · level 3

Show the name of each member and their number of loans, including 0 for those who borrowed nothing.

Table members (6 rows)
idnamecityjoined
1LenaLyon2022-01-15
2MarcParis2021-06-03
3NadiaLyon2023-03-20
4OscarLille2020-11-11
5PaulaParis2024-02-01
6QuentinNantes2023-09-09
Table loans (12 rows)
idbook_idmember_idloan_datereturn_date
1112025-01-052025-01-19
2322025-01-102025-02-02
3512025-02-012025-02-10
4732025-02-03NULL
5342025-02-152025-03-01
6222025-03-022025-03-30
7852025-03-05NULL
8132025-03-102025-03-18
9612025-03-202025-04-15
10352025-04-01NULL
11942025-04-052025-04-12
121022025-04-082025-04-20
Show the hint

Topics to use: COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , COUNT()
FROM  
LEFT JOIN   ON  = 
GROUP BY 
Show the solution
SELECT m.name, COUNT(l.id)
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
GROUP BY m.id;

Expected result (6 rows):

nameCOUNT(l.id)
Lena3
Marc3
Nadia2
Oscar2
Paula2
Quentin0

Exercise 6 · level 3

Show the date of each match with the name of the home team and the name of the away team.

Table matches (10 rows)
idplayed_onhome_idaway_idhome_goalsaway_goals
12025-08-021221
22025-08-033400
32025-08-095113
42025-08-102322
52025-08-164531
62025-08-171310
72025-08-232401
82025-08-243523
92025-08-304111
102025-08-315202
Table teams (5 rows)
idnamecityfounded
1Red FoxesLyon1950
2Blue OwlsParis1962
3Green BullsLille1971
4Gold HawksNantes1988
5Grey WolvesParis1990
Show the hint

Topics to use: JOIN.

Query structure:

SELECT , , 
FROM  
JOIN   ON  = 
JOIN   ON  = 
Show the solution
SELECT m.played_on, h.name, a.name
FROM matches m
JOIN teams h ON h.id = m.home_id
JOIN teams a ON a.id = m.away_id;

Expected result (10 rows):

played_onnamename
2025-08-02Red FoxesBlue Owls
2025-08-03Green BullsGold Hawks
2025-08-09Grey WolvesRed Foxes
2025-08-10Blue OwlsGreen Bulls
2025-08-16Gold HawksGrey Wolves
2025-08-17Red FoxesGreen Bulls
2025-08-23Blue OwlsGold Hawks
2025-08-24Green BullsGrey Wolves
2025-08-30Gold HawksRed Foxes
2025-08-31Grey WolvesBlue Owls

Exercise 7 · level 3

Show the name of each artist and their number of songs, including 0 for those who have none.

Table artists (6 rows)
idnamecountrydebut_year
1Luna ValeUSA2012
2The StaticUK2005
3Kofi MensahGhana2016
4Mira SolSpain2010
5Neon HarborCanada2018
6Iris NovaFrance2020
Table songs (10 rows)
idtitleartist_idgenreduration_sreleased
1Glass Heart1Pop2142019-04-12
2Low Tide1Pop1982021-06-01
3Rust2Rock2562008-09-30
4Wires2Rock3012015-02-14
5Sunday Market3Afrobeat2332020-11-20
6Palm Wine3Afrobeat2452022-03-03
7Alma4Latin1892013-07-07
8Brisa4Pop2052018-05-25
9Night Drive5Electro2762021-10-10
10Pulse5Electro2302023-01-15
Show the hint

Topics to use: COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , COUNT()
FROM  
LEFT JOIN   ON  = 
GROUP BY 
Show the solution
SELECT a.name, COUNT(s.id)
FROM artists a
LEFT JOIN songs s ON s.artist_id = a.id
GROUP BY a.id;

Expected result (6 rows):

nameCOUNT(s.id)
Luna Vale2
The Static2
Kofi Mensah2
Mira Sol2
Neon Harbor2
Iris Nova0

Exercise 8 · level 3

Show the name of each guest and their number of bookings, including 0 for those who never booked.

Table guests (6 rows)
idnamecountry
1Ana SilvaPortugal
2Ben FordUSA
3Chen LiChina
4Dana WeissGermany
5Emma RoyFrance
6Farid NasserMorocco
Table bookings (11 rows)
idroom_idguest_idcheck_incheck_out
1212025-07-012025-07-04
2522025-07-022025-07-05
3832025-07-032025-07-06
4342025-07-052025-07-12
5652025-07-062025-07-08
6122025-07-082025-07-10
7932025-07-102025-07-11
8752025-07-112025-07-15
9412025-07-142025-07-16
10642025-07-152025-07-19
11652025-07-162025-07-18
Show the hint

Topics to use: COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , COUNT()
FROM  
LEFT JOIN   ON  = 
GROUP BY 
Show the solution
SELECT g.name, COUNT(b.id)
FROM guests g
LEFT JOIN bookings b ON b.guest_id = g.id
GROUP BY g.id;

Expected result (6 rows):

nameCOUNT(b.id)
Ana Silva2
Ben Ford2
Chen Li2
Dana Weiss2
Emma Roy3
Farid Nasser0

Exercise 9 · level 3

For each order, show its id and its total amount (quantity × price), from the largest amount to the smallest (on ties, by id).

Table order_items (14 rows)
order_idproduct_idqty
111
122
241
335
321
451
562
511
624
633
741
761
852
821
Table products (7 rows)
idnamecategoryprice
1Desk LampHome35
2Coffee MugKitchen12
3NotebookOffice6
4Office ChairOffice149
5KettleKitchen45
6CushionHome22
7StaplerOffice9
Show the hint

Topics to use: ORDER BY, LIMIT, COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , SUM( * )
FROM  
JOIN   ON  = 
GROUP BY 
ORDER BY SUM( * ) DESC, 
Show the solution
SELECT oi.order_id, SUM(oi.qty * p.price)
FROM order_items oi
JOIN products p ON p.id = oi.product_id
GROUP BY oi.order_id
ORDER BY SUM(oi.qty * p.price) DESC, oi.order_id;

Expected result (8 rows, in this order):

order_idSUM(oi.qty * p.price)
7171
2149
8102
579
666
159
445
342

Exercise 10 · level 3

For each project with tasks, show its name and the total hours of its tasks, from the largest total to the smallest (on ties, by name).

Table projects (4 rows)
idnameclientbudgetdeadline
1AtlasAcme200002025-06-30
2BeaconBolt120002025-05-15
3CometAcme80002025-04-30
4DeltaCyan150002025-07-31
Table tasks (10 rows)
idproject_iddev_idtitlehoursstatusdone_on
111Landing page12done2025-03-10
213Data model20done2025-03-20
312Login8doingNULL
424ETL job16done2025-04-02
525CI pipeline6done2025-03-15
631Dashboard14todoNULL
733Report10done2025-04-25
842API18doingNULL
945Monitoring9todoNULL
1044Forecast22done2025-05-05
Show the hint

Topics to use: ORDER BY, LIMIT, COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , SUM()
FROM  
JOIN   ON  = 
GROUP BY 
ORDER BY SUM() DESC, 
Show the solution
SELECT p.name, SUM(t.hours)
FROM projects p
JOIN tasks t ON t.project_id = p.id
GROUP BY p.id
ORDER BY SUM(t.hours) DESC, p.name;

Expected result (4 rows, in this order):

nameSUM(t.hours)
Delta49
Atlas40
Comet24
Beacon22

Exercise 11 · level 3

For each owner with transactions, show their name and their total balance (all their accounts), from the largest to the smallest (on ties, by name).

Table accounts (6 rows)
idownercityopenedkind
1AliceParis2021-03-01current
2AliceParis2022-06-15savings
3BrunoLyon2020-09-10current
4ChloeLyon2023-01-20current
5DavidNice2019-11-05savings
6EmmaNice2024-04-01current
Table transactions (14 rows)
idaccount_idmade_onamountlabel
112025-01-022500salary
212025-01-05-60groceries
312025-01-12-800rent
422025-01-15500transfer
532025-01-031900salary
632025-01-20-120groceries
732025-02-01-950rent
842025-01-252100salary
942025-02-03-45restaurant
1012025-02-022600salary
1112025-02-06-75groceries
1252025-02-1030interest
1322025-02-15500transfer
1442025-02-18-600rent
Show the hint

Topics to use: ORDER BY, LIMIT, COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , SUM()
FROM  
JOIN   ON  = 
GROUP BY 
ORDER BY SUM() DESC, 
Show the solution
SELECT a.owner, SUM(t.amount)
FROM accounts a
JOIN transactions t ON t.account_id = a.id
GROUP BY a.owner
ORDER BY SUM(t.amount) DESC, a.owner;

Expected result (4 rows, in this order):

ownerSUM(t.amount)
Alice5165
Chloe1455
Bruno830
David30

Exercise 12 · level 5

Show each employee with their manager's name (ignore those who have no manager).

Table staff (10 rows)
idnamedepartmentsalarymanager_idhired
1AliceFinance52000NULL2015-03-01
2BobIT4100012018-06-15
3ClaireFinance3800012019-01-10
4DavidHR2900012020-09-01
5EmmaIT5000022017-11-20
6FaridIT4700022021-02-14
7GaelleHR3300042022-05-30
8HugoFinance4400032016-08-08
9IrisIT4700052023-01-09
10JulesFinance4400032024-03-18
Show the hint

Topics to use: JOIN.

Query structure:

SELECT , 
FROM  
JOIN   ON  = 
Show the solution
SELECT e.name, m.name
FROM staff e
JOIN staff m ON e.manager_id = m.id;

Expected result (9 rows):

namename
BobAlice
ClaireAlice
DavidAlice
EmmaBob
FaridBob
GaelleDavid
HugoClaire
IrisEmma
JulesClaire

Exercise 13 · level 5

Show the name of the employees who were hired before their own manager.

Table staff (10 rows)
idnamedepartmentsalarymanager_idhired
1AliceFinance52000NULL2015-03-01
2BobIT4100012018-06-15
3ClaireFinance3800012019-01-10
4DavidHR2900012020-09-01
5EmmaIT5000022017-11-20
6FaridIT4700022021-02-14
7GaelleHR3300042022-05-30
8HugoFinance4400032016-08-08
9IrisIT4700052023-01-09
10JulesFinance4400032024-03-18
Show the hint

Topics to use: WHERE (filters), JOIN.

Query structure:

SELECT 
FROM  
JOIN   ON  = 
WHERE  < 
Show the solution
SELECT e.name
FROM staff e
JOIN staff m ON e.manager_id = m.id
WHERE e.hired < m.hired;

Expected result (2 rows):

name
Emma
Hugo

Exercise 14 · level 5

Show the pairs of players from the same team (columns: name 1, name 2) where player 1 scored more goals than player 2.

Table players (9 rows)
idnameteam_idpositiongoals
1Alex Moreau1FW9
2Bilal Sow1MF4
3Carl Weber2FW7
4Diego Ruiz2DF1
5Eli Novak3FW6
6Femi Adeyemi4FW11
7Goran Petrov4GK0
8Hugo Lamy5MF5
9Ivan Kral5DF2
Show the hint

Topics to use: JOIN.

Query structure:

SELECT , 
FROM  
JOIN   ON  =  AND  > 
Show the solution
SELECT a.name, b.name
FROM players a
JOIN players b ON a.team_id = b.team_id AND a.goals > b.goals;

Expected result (4 rows):

namename
Alex MoreauBilal Sow
Carl WeberDiego Ruiz
Femi AdeyemiGoran Petrov
Hugo LamyIvan Kral

Exercise 15 · level 8

Show the pairs of products bought together in the same order (the name of the product with the smaller id first), with the number of orders in which they appear together.

Table order_items (14 rows)
order_idproduct_idqty
111
122
241
335
321
451
562
511
624
633
741
761
852
821
Table products (7 rows)
idnamecategoryprice
1Desk LampHome35
2Coffee MugKitchen12
3NotebookOffice6
4Office ChairOffice149
5KettleKitchen45
6CushionHome22
7StaplerOffice9
Show the hint

Topics to use: COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN.

Query structure:

SELECT , , COUNT(*)
FROM  
JOIN   ON  =  AND  < 
JOIN   ON  = 
JOIN   ON  = 
GROUP BY , 
Show the solution
SELECT p1.name, p2.name, COUNT(*)
FROM order_items a
JOIN order_items b ON b.order_id = a.order_id AND a.product_id < b.product_id
JOIN products p1 ON p1.id = a.product_id
JOIN products p2 ON p2.id = b.product_id
GROUP BY a.product_id, b.product_id;

Expected result (5 rows):

namenameCOUNT(*)
Desk LampCoffee Mug1
Desk LampCushion1
Coffee MugNotebook2
Coffee MugKettle1
Office ChairCushion1

Practice with automatic checking

In SpeedQL, you write your query and it is checked straight away, on these tables and then on a hidden dataset.

Practice on the 60 “JOIN” questions