SQL exercises with solutions: window functions
Updated on
Window functions compute over a set of rows without grouping them: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM() OVER… These 15 exercises range from level 6 to level 8; they use the syntax of SQLite, SpeedQL’s SQL engine.
Tip: PARTITION BY splits the groups, ORDER BY sets the order inside each group.
Read the “Window functions (OVER)” card in the cheat sheet
Exercise 1
Number the employees from the highest to the lowest paid (on ties, alphabetical order of the name). Show name, salary and the number. Use ROW_NUMBER().
| id | name | department | salary | manager_id | hired |
|---|---|---|---|---|---|
| 1 | Alice | Finance | 52000 | NULL | 2015-03-01 |
| 2 | Bob | IT | 41000 | 1 | 2018-06-15 |
| 3 | Claire | Finance | 38000 | 1 | 2019-01-10 |
| 4 | David | HR | 29000 | 1 | 2020-09-01 |
| 5 | Emma | IT | 50000 | 2 | 2017-11-20 |
| 6 | Farid | IT | 47000 | 2 | 2021-02-14 |
| 7 | Gaelle | HR | 33000 | 4 | 2022-05-30 |
| 8 | Hugo | Finance | 44000 | 3 | 2016-08-08 |
| 9 | Iris | IT | 47000 | 5 | 2023-01-09 |
| 10 | Jules | Finance | 44000 | 3 | 2024-03-18 |
Show the hint
Topics to use: Window functions (OVER).
Query structure:
SELECT …, …, ROW_NUMBER() OVER (ORDER BY … DESC, …)
FROM …Show the solution
SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC, name)
FROM staff;Expected result (10 rows):
| name | salary | ROW_NUMBER() OVER (ORDER BY salary DESC, name) |
|---|---|---|
| Alice | 52000 | 1 |
| Emma | 50000 | 2 |
| Farid | 47000 | 3 |
| Iris | 47000 | 4 |
| Hugo | 44000 | 5 |
| Jules | 44000 | 6 |
| Bob | 41000 | 7 |
| Claire | 38000 | 8 |
| Gaelle | 33000 | 9 |
| David | 29000 | 10 |
Exercise 2
Within each department, number the employees from the highest to the lowest paid (ties: alphabetical order of the name). Show name, department and the number. Use ROW_NUMBER().
| id | name | department | salary | manager_id | hired |
|---|---|---|---|---|---|
| 1 | Alice | Finance | 52000 | NULL | 2015-03-01 |
| 2 | Bob | IT | 41000 | 1 | 2018-06-15 |
| 3 | Claire | Finance | 38000 | 1 | 2019-01-10 |
| 4 | David | HR | 29000 | 1 | 2020-09-01 |
| 5 | Emma | IT | 50000 | 2 | 2017-11-20 |
| 6 | Farid | IT | 47000 | 2 | 2021-02-14 |
| 7 | Gaelle | HR | 33000 | 4 | 2022-05-30 |
| 8 | Hugo | Finance | 44000 | 3 | 2016-08-08 |
| 9 | Iris | IT | 47000 | 5 | 2023-01-09 |
| 10 | Jules | Finance | 44000 | 3 | 2024-03-18 |
Show the hint
Topics to use: Window functions (OVER).
Query structure:
SELECT …, …, ROW_NUMBER() OVER (PARTITION BY … ORDER BY … DESC, …)
FROM …Show the solution
SELECT name, department, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC, name)
FROM staff;Expected result (10 rows):
| name | department | ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC, name) |
|---|---|---|
| Alice | Finance | 1 |
| Hugo | Finance | 2 |
| Jules | Finance | 3 |
| Claire | Finance | 4 |
| Gaelle | HR | 1 |
| David | HR | 2 |
| Emma | IT | 1 |
| Farid | IT | 2 |
| Iris | IT | 3 |
| Bob | IT | 4 |
Exercise 3
Show the title, the rating and the gap between the movie's rating and the average rating of all movies, rounded to 2 decimals. Use a window function.
| id | title | genre | year | duration | rating | director_id |
|---|---|---|---|---|---|---|
| 1 | Night Train | Thriller | 2015 | 118 | 7.8 | 1 |
| 2 | Blue Harbor | Drama | 2018 | 102 | 7.1 | 2 |
| 3 | Paper Moon City | Comedy | 2012 | 95 | 6.4 | 5 |
| 4 | Silent Peak | Drama | 2020 | 131 | 8.2 | 3 |
| 5 | Last Signal | Sci-Fi | 2019 | 142 | 7.5 | 1 |
| 6 | Summer Keys | Comedy | 2016 | 88 | 5.9 | 4 |
| 7 | Iron Garden | Sci-Fi | 2021 | 125 | 8 | 3 |
| 8 | Dust and Gold | Western | 2014 | 110 | 6.8 | 2 |
| 9 | The Quiet Hour | Drama | 2022 | 97 | 7.4 | 5 |
| 10 | Deep Current | Thriller | 2017 | 105 | 6.9 | NULL |
Show the hint
Topics to use: Window functions (OVER).
Query structure:
SELECT …, …, ROUND(… - AVG(…) OVER (), …)
FROM …Show the solution
SELECT title, rating, ROUND(rating - AVG(rating) OVER (), 2)
FROM movies;Expected result (10 rows):
| title | rating | ROUND(rating - AVG(rating) OVER (), 2) |
|---|---|---|
| Night Train | 7.8 | 0.6 |
| Blue Harbor | 7.1 | -0.1 |
| Paper Moon City | 6.4 | -0.8 |
| Silent Peak | 8.2 | 1 |
| Last Signal | 7.5 | 0.3 |
| Summer Keys | 5.9 | -1.3 |
| Iron Garden | 8 | 0.8 |
| Dust and Gold | 6.8 | -0.4 |
| The Quiet Hour | 7.4 | 0.2 |
| Deep Current | 6.9 | -0.3 |
Exercise 4
Show the date of each match and the running number of goals scored in the league up to and including that date (window function).
| id | played_on | home_id | away_id | home_goals | away_goals |
|---|---|---|---|---|---|
| 1 | 2025-08-02 | 1 | 2 | 2 | 1 |
| 2 | 2025-08-03 | 3 | 4 | 0 | 0 |
| 3 | 2025-08-09 | 5 | 1 | 1 | 3 |
| 4 | 2025-08-10 | 2 | 3 | 2 | 2 |
| 5 | 2025-08-16 | 4 | 5 | 3 | 1 |
| 6 | 2025-08-17 | 1 | 3 | 1 | 0 |
| 7 | 2025-08-23 | 2 | 4 | 0 | 1 |
| 8 | 2025-08-24 | 3 | 5 | 2 | 3 |
| 9 | 2025-08-30 | 4 | 1 | 1 | 1 |
| 10 | 2025-08-31 | 5 | 2 | 0 | 2 |
Show the hint
Topics to use: Window functions (OVER).
Query structure:
SELECT …, SUM(… + …) OVER (ORDER BY …)
FROM …Show the solution
SELECT played_on, SUM(home_goals + away_goals) OVER (ORDER BY played_on)
FROM matches;Expected result (10 rows):
| played_on | SUM(home_goals + away_goals) OVER (ORDER BY played_on) |
|---|---|
| 2025-08-02 | 3 |
| 2025-08-03 | 3 |
| 2025-08-09 | 7 |
| 2025-08-10 | 11 |
| 2025-08-16 | 15 |
| 2025-08-17 | 16 |
| 2025-08-23 | 17 |
| 2025-08-24 | 22 |
| 2025-08-30 | 24 |
| 2025-08-31 | 26 |
Exercise 5
Show the id, the destination, the price and the average price of the flights to the same destination, rounded to 1 decimal (window function).
| id | airline | origin | dest | departs | duration_min | price | seats_sold | capacity |
|---|---|---|---|---|---|---|---|---|
| 1 | SkyJet | CDG | MAD | 2025-06-01 08:10 | 125 | 89 | 150 | 180 |
| 2 | AirNova | CDG | LIS | 2025-06-01 11:40 | 155 | 120 | 160 | 170 |
| 3 | BlueWing | LYS | FCO | 2025-06-02 07:30 | 95 | 75 | 110 | 150 |
| 4 | SkyJet | MAD | CDG | 2025-06-02 18:20 | 120 | 95 | 170 | 180 |
| 5 | AirNova | BER | CDG | 2025-06-03 09:05 | 110 | 105 | 140 | 160 |
| 6 | BlueWing | NCE | BER | 2025-06-03 13:50 | 130 | 140 | 90 | 150 |
| 7 | SkyJet | CDG | FCO | 2025-06-04 06:45 | 135 | 99 | 175 | 180 |
| 8 | AirNova | LIS | MAD | 2025-06-04 16:15 | 75 | 65 | 60 | 120 |
| 9 | BlueWing | FCO | LYS | 2025-06-05 20:30 | 100 | 82 | 130 | 150 |
| 10 | SkyJet | CDG | BER | 2025-06-05 10:00 | 105 | 110 | 120 | 180 |
| 11 | AirNova | MAD | LIS | 2025-06-06 12:25 | 80 | 70 | 95 | 120 |
| 12 | BlueWing | LYS | MAD | 2025-06-06 15:40 | 115 | 79 | 100 | 150 |
Show the hint
Topics to use: Window functions (OVER).
Query structure:
SELECT …, …, …, ROUND(AVG(…) OVER (PARTITION BY …), …)
FROM …Show the solution
SELECT id, dest, price, ROUND(AVG(price) OVER (PARTITION BY dest), 1)
FROM flights;Expected result (12 rows):
| id | dest | price | ROUND(AVG(price) OVER (PARTITION BY dest), 1) |
|---|---|---|---|
| 6 | BER | 140 | 125 |
| 10 | BER | 110 | 125 |
| 4 | CDG | 95 | 100 |
| 5 | CDG | 105 | 100 |
| 3 | FCO | 75 | 87 |
| 7 | FCO | 99 | 87 |
| 2 | LIS | 120 | 95 |
| 11 | LIS | 70 | 95 |
| 9 | LYS | 82 | 82 |
| 1 | MAD | 89 | 77.7 |
| 8 | MAD | 65 | 77.7 |
| 12 | MAD | 79 | 77.7 |
Exercise 6
Show the city, the day, that day's rain and the next day's rain in the same city (NULL for the last day). Use LEAD().
| id | city | day | temp_max | temp_min | rain_mm |
|---|---|---|---|---|---|
| 1 | Paris | 2025-07-01 | 24 | 15 | 0 |
| 2 | Paris | 2025-07-02 | 27 | 17 | 0 |
| 3 | Paris | 2025-07-03 | 22 | 16 | 4.5 |
| 4 | Paris | 2025-07-04 | 19 | 14 | 12 |
| 5 | Paris | 2025-07-05 | 23 | 13 | 0 |
| 6 | Lyon | 2025-07-01 | 26 | 16 | 0 |
| 7 | Lyon | 2025-07-02 | 29 | 18 | 0 |
| 8 | Lyon | 2025-07-03 | 31 | 19 | 0 |
| 9 | Lyon | 2025-07-04 | 24 | 17 | 8.5 |
| 10 | Lyon | 2025-07-05 | 22 | 15 | 3 |
| 11 | Marseille | 2025-07-01 | 30 | 21 | 0 |
| 12 | Marseille | 2025-07-02 | 32 | 22 | 0 |
| 13 | Marseille | 2025-07-03 | 33 | 23 | 0 |
| 14 | Marseille | 2025-07-04 | 29 | 21 | 0 |
| 15 | Marseille | 2025-07-05 | 28 | 20 | 0 |
Show the hint
Topics to use: Window functions (OVER).
Query structure:
SELECT …, …, …, LEAD(…) OVER (PARTITION BY … ORDER BY …)
FROM …Show the solution
SELECT city, day, rain_mm, LEAD(rain_mm) OVER (PARTITION BY city ORDER BY day)
FROM readings;Expected result (15 rows):
| city | day | rain_mm | LEAD(rain_mm) OVER (PARTITION BY city ORDER BY day) |
|---|---|---|---|
| Lyon | 2025-07-01 | 0 | 0 |
| Lyon | 2025-07-02 | 0 | 0 |
| Lyon | 2025-07-03 | 0 | 8.5 |
| Lyon | 2025-07-04 | 8.5 | 3 |
| Lyon | 2025-07-05 | 3 | NULL |
| Marseille | 2025-07-01 | 0 | 0 |
| Marseille | 2025-07-02 | 0 | 0 |
| Marseille | 2025-07-03 | 0 | 0 |
| Marseille | 2025-07-04 | 0 | 0 |
| Marseille | 2025-07-05 | 0 | NULL |
| Paris | 2025-07-01 | 0 | 0 |
| Paris | 2025-07-02 | 0 | 4.5 |
| Paris | 2025-07-03 | 4.5 | 12 |
| Paris | 2025-07-04 | 12 | 0 |
| Paris | 2025-07-05 | 0 | NULL |
Exercise 7
For each order that is not cancelled, show its id, its date, its amount and the running revenue (by date, then by id).
| id | customer_id | order_date | status |
|---|---|---|---|
| 1 | 1 | 2025-01-05 | shipped |
| 2 | 2 | 2025-01-12 | shipped |
| 3 | 1 | 2025-02-03 | paid |
| 4 | 3 | 2025-02-10 | cancelled |
| 5 | 4 | 2025-02-20 | shipped |
| 6 | 5 | 2025-03-02 | shipped |
| 7 | 2 | 2025-03-15 | paid |
| 8 | 3 | 2025-03-28 | shipped |
| order_id | product_id | qty |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 2 | 2 |
| 2 | 4 | 1 |
| 3 | 3 | 5 |
| 3 | 2 | 1 |
| 4 | 5 | 1 |
| 5 | 6 | 2 |
| 5 | 1 | 1 |
| 6 | 2 | 4 |
| 6 | 3 | 3 |
| 7 | 4 | 1 |
| 7 | 6 | 1 |
| 8 | 5 | 2 |
| 8 | 2 | 1 |
| id | name | category | price |
|---|---|---|---|
| 1 | Desk Lamp | Home | 35 |
| 2 | Coffee Mug | Kitchen | 12 |
| 3 | Notebook | Office | 6 |
| 4 | Office Chair | Office | 149 |
| 5 | Kettle | Kitchen | 45 |
| 6 | Cushion | Home | 22 |
| 7 | Stapler | Office | 9 |
Show the hint
Topics to use: WHERE (filters), COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN, CTEs (WITH), Window functions (OVER).
Query structure:
WITH … AS (
SELECT …, …, SUM(… * …) AS …
FROM … …
JOIN … … ON … = …
JOIN … … ON … = …
WHERE … <> …
GROUP BY …)
SELECT …, …, …, SUM(…) OVER (ORDER BY …, …)
FROM …Show the solution
WITH t AS (
SELECT o.id, o.order_date, SUM(oi.qty * p.price) AS amount
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY o.id)
SELECT id, order_date, amount, SUM(amount) OVER (ORDER BY order_date, id)
FROM t;Expected result (7 rows):
| id | order_date | amount | SUM(amount) OVER (ORDER BY order_date, id) |
|---|---|---|---|
| 1 | 2025-01-05 | 59 | 59 |
| 2 | 2025-01-12 | 149 | 208 |
| 3 | 2025-02-03 | 42 | 250 |
| 5 | 2025-02-20 | 79 | 329 |
| 6 | 2025-03-02 | 66 | 395 |
| 7 | 2025-03-15 | 171 | 566 |
| 8 | 2025-03-28 | 102 | 668 |
Exercise 8
For each transaction, show the account, the date, the amount and the account balance after the transaction (running total by date, then by id).
| id | account_id | made_on | amount | label |
|---|---|---|---|---|
| 1 | 1 | 2025-01-02 | 2500 | salary |
| 2 | 1 | 2025-01-05 | -60 | groceries |
| 3 | 1 | 2025-01-12 | -800 | rent |
| 4 | 2 | 2025-01-15 | 500 | transfer |
| 5 | 3 | 2025-01-03 | 1900 | salary |
| 6 | 3 | 2025-01-20 | -120 | groceries |
| 7 | 3 | 2025-02-01 | -950 | rent |
| 8 | 4 | 2025-01-25 | 2100 | salary |
| 9 | 4 | 2025-02-03 | -45 | restaurant |
| 10 | 1 | 2025-02-02 | 2600 | salary |
| 11 | 1 | 2025-02-06 | -75 | groceries |
| 12 | 5 | 2025-02-10 | 30 | interest |
| 13 | 2 | 2025-02-15 | 500 | transfer |
| 14 | 4 | 2025-02-18 | -600 | rent |
Show the hint
Topics to use: Window functions (OVER).
Query structure:
SELECT …, …, …, SUM(…) OVER (PARTITION BY … ORDER BY …, …)
FROM …Show the solution
SELECT account_id, made_on, amount, SUM(amount) OVER (PARTITION BY account_id ORDER BY made_on, id)
FROM transactions;Expected result (14 rows):
| account_id | made_on | amount | SUM(amount) OVER (PARTITION BY account_id ORDER BY made_on, id) |
|---|---|---|---|
| 1 | 2025-01-02 | 2500 | 2500 |
| 1 | 2025-01-05 | -60 | 2440 |
| 1 | 2025-01-12 | -800 | 1640 |
| 1 | 2025-02-02 | 2600 | 4240 |
| 1 | 2025-02-06 | -75 | 4165 |
| 2 | 2025-01-15 | 500 | 500 |
| 2 | 2025-02-15 | 500 | 1000 |
| 3 | 2025-01-03 | 1900 | 1900 |
| 3 | 2025-01-20 | -120 | 1780 |
| 3 | 2025-02-01 | -950 | 830 |
| 4 | 2025-01-25 | 2100 | 2100 |
| 4 | 2025-02-03 | -45 | 2055 |
| 4 | 2025-02-18 | -600 | 1455 |
| 5 | 2025-02-10 | 30 | 30 |
Exercise 9
Compute the median salary of all employees (the average of the two middle values when the number of employees is even).
| id | name | department | salary | manager_id | hired |
|---|---|---|---|---|---|
| 1 | Alice | Finance | 52000 | NULL | 2015-03-01 |
| 2 | Bob | IT | 41000 | 1 | 2018-06-15 |
| 3 | Claire | Finance | 38000 | 1 | 2019-01-10 |
| 4 | David | HR | 29000 | 1 | 2020-09-01 |
| 5 | Emma | IT | 50000 | 2 | 2017-11-20 |
| 6 | Farid | IT | 47000 | 2 | 2021-02-14 |
| 7 | Gaelle | HR | 33000 | 4 | 2022-05-30 |
| 8 | Hugo | Finance | 44000 | 3 | 2016-08-08 |
| 9 | Iris | IT | 47000 | 5 | 2023-01-09 |
| 10 | Jules | Finance | 44000 | 3 | 2024-03-18 |
Show the hint
Topics to use: WHERE (filters), COUNT, SUM, AVG, MIN, MAX, Subqueries, Window functions (OVER).
Query structure:
SELECT AVG(…)
FROM (SELECT …, ROW_NUMBER() OVER (ORDER BY …) AS …, COUNT(*) OVER () AS … FROM …)
WHERE … IN ((… + …) / …, (… + …) / …)Show the solution
SELECT AVG(salary)
FROM (SELECT salary, ROW_NUMBER() OVER (ORDER BY salary) AS rn, COUNT(*) OVER () AS c FROM staff)
WHERE rn IN ((c + 1) / 2, (c + 2) / 2);Expected result (1 row):
| AVG(salary) |
|---|
| 44000 |
Exercise 10
For each team, show the name of its top scorer (columns: team_id, name).
| id | name | team_id | position | goals |
|---|---|---|---|---|
| 1 | Alex Moreau | 1 | FW | 9 |
| 2 | Bilal Sow | 1 | MF | 4 |
| 3 | Carl Weber | 2 | FW | 7 |
| 4 | Diego Ruiz | 2 | DF | 1 |
| 5 | Eli Novak | 3 | FW | 6 |
| 6 | Femi Adeyemi | 4 | FW | 11 |
| 7 | Goran Petrov | 4 | GK | 0 |
| 8 | Hugo Lamy | 5 | MF | 5 |
| 9 | Ivan Kral | 5 | DF | 2 |
Show the hint
Topics to use: WHERE (filters), Subqueries, Window functions (OVER).
Query structure:
SELECT …, …
FROM (SELECT …, …, ROW_NUMBER() OVER (PARTITION BY … ORDER BY … DESC) AS … FROM …)
WHERE … = …Show the solution
SELECT team_id, name
FROM (SELECT team_id, name, ROW_NUMBER() OVER (PARTITION BY team_id ORDER BY goals DESC) AS rn FROM players)
WHERE rn = 1;Expected result (5 rows):
| team_id | name |
|---|---|
| 1 | Alex Moreau |
| 2 | Carl Weber |
| 3 | Eli Novak |
| 4 | Femi Adeyemi |
| 5 | Hugo Lamy |
Exercise 11
For each city with at least one dry day, show the city and the longest run of consecutive dry days (rain_mm = 0).
| id | city | day | temp_max | temp_min | rain_mm |
|---|---|---|---|---|---|
| 1 | Paris | 2025-07-01 | 24 | 15 | 0 |
| 2 | Paris | 2025-07-02 | 27 | 17 | 0 |
| 3 | Paris | 2025-07-03 | 22 | 16 | 4.5 |
| 4 | Paris | 2025-07-04 | 19 | 14 | 12 |
| 5 | Paris | 2025-07-05 | 23 | 13 | 0 |
| 6 | Lyon | 2025-07-01 | 26 | 16 | 0 |
| 7 | Lyon | 2025-07-02 | 29 | 18 | 0 |
| 8 | Lyon | 2025-07-03 | 31 | 19 | 0 |
| 9 | Lyon | 2025-07-04 | 24 | 17 | 8.5 |
| 10 | Lyon | 2025-07-05 | 22 | 15 | 3 |
| 11 | Marseille | 2025-07-01 | 30 | 21 | 0 |
| 12 | Marseille | 2025-07-02 | 32 | 22 | 0 |
| 13 | Marseille | 2025-07-03 | 33 | 23 | 0 |
| 14 | Marseille | 2025-07-04 | 29 | 21 | 0 |
| 15 | Marseille | 2025-07-05 | 28 | 20 | 0 |
Show the hint
Topics to use: WHERE (filters), COUNT, SUM, AVG, MIN, MAX, GROUP BY, CTEs (WITH), Window functions (OVER).
Query structure:
WITH … AS (
SELECT …, …, …, ROW_NUMBER() OVER (PARTITION BY … ORDER BY …) - ROW_NUMBER() OVER (PARTITION BY …, … = … ORDER BY …) AS …
FROM …), … AS (
SELECT …, …, COUNT(*) AS …
FROM …
WHERE … = …
GROUP BY …, …)
SELECT …, MAX(…)
FROM …
GROUP BY …Show the solution
WITH r AS (
SELECT city, day, rain_mm, ROW_NUMBER() OVER (PARTITION BY city ORDER BY day) - ROW_NUMBER() OVER (PARTITION BY city, rain_mm = 0 ORDER BY day) AS grp
FROM readings), s AS (
SELECT city, grp, COUNT(*) AS n
FROM r
WHERE rain_mm = 0
GROUP BY city, grp)
SELECT city, MAX(n)
FROM s
GROUP BY city;Expected result (3 rows):
| city | MAX(n) |
|---|---|
| Lyon | 3 |
| Marseille | 5 |
| Paris | 2 |
Exercise 12
Give the first month in which the running total of the North region sales exceeds 1000 (a single result).
| id | seller | region | month | amount |
|---|---|---|---|---|
| 1 | Ana | North | 2025-01 | 300 |
| 2 | Ana | North | 2025-02 | 450 |
| 3 | Ana | North | 2025-03 | 400 |
| 4 | Ben | North | 2025-01 | 500 |
| 5 | Ben | North | 2025-02 | 350 |
| 6 | Ben | North | 2025-03 | 600 |
| 7 | Cleo | South | 2025-01 | 200 |
| 8 | Cleo | South | 2025-02 | 700 |
| 9 | Cleo | South | 2025-03 | 650 |
| 10 | Dan | South | 2025-01 | 400 |
| 11 | Dan | South | 2025-02 | 400 |
| 12 | Dan | South | 2025-03 | 100 |
Show the hint
Topics to use: WHERE (filters), COUNT, SUM, AVG, MIN, MAX, GROUP BY, CTEs (WITH), Window functions (OVER).
Query structure:
WITH … AS (
SELECT …, SUM(SUM(…)) OVER (ORDER BY …) AS …
FROM …
WHERE … = …
GROUP BY …)
SELECT MIN(…)
FROM …
WHERE … > …Show the solution
WITH m AS (
SELECT month, SUM(SUM(amount)) OVER (ORDER BY month) AS c
FROM sales
WHERE region = 'North'
GROUP BY month)
SELECT MIN(month)
FROM m
WHERE c > 1000;Expected result (1 row):
| MIN(month) |
|---|
| 2025-02 |
Exercise 13
For each month with loans (format YYYY-MM), show the month, the number of loans that month and the running total of loans since the first month, in chronological order.
| id | book_id | member_id | loan_date | return_date |
|---|---|---|---|---|
| 1 | 1 | 1 | 2025-01-05 | 2025-01-19 |
| 2 | 3 | 2 | 2025-01-10 | 2025-02-02 |
| 3 | 5 | 1 | 2025-02-01 | 2025-02-10 |
| 4 | 7 | 3 | 2025-02-03 | NULL |
| 5 | 3 | 4 | 2025-02-15 | 2025-03-01 |
| 6 | 2 | 2 | 2025-03-02 | 2025-03-30 |
| 7 | 8 | 5 | 2025-03-05 | NULL |
| 8 | 1 | 3 | 2025-03-10 | 2025-03-18 |
| 9 | 6 | 1 | 2025-03-20 | 2025-04-15 |
| 10 | 3 | 5 | 2025-04-01 | NULL |
| 11 | 9 | 4 | 2025-04-05 | 2025-04-12 |
| 12 | 10 | 2 | 2025-04-08 | 2025-04-20 |
Show the hint
Topics to use: ORDER BY, LIMIT, COUNT, SUM, AVG, MIN, MAX, GROUP BY, Text and dates, Window functions (OVER).
Query structure:
SELECT STRFTIME(…, …) AS …, COUNT(*), SUM(COUNT(*)) OVER (ORDER BY STRFTIME(…, …))
FROM …
GROUP BY …
ORDER BY …Show the solution
SELECT strftime('%Y-%m', loan_date) AS month, COUNT(*), SUM(COUNT(*)) OVER (ORDER BY strftime('%Y-%m', loan_date))
FROM loans
GROUP BY month
ORDER BY month;Expected result (4 rows, in this order):
| month | COUNT(*) | SUM(COUNT(*)) OVER (ORDER BY strftime('%Y-%m', loan_date)) |
|---|---|---|
| 2025-01 | 2 | 2 |
| 2025-02 | 3 | 5 |
| 2025-03 | 4 | 9 |
| 2025-04 | 3 | 12 |
Exercise 14
For each region with readings, show the region, the number of rainy days, the total rain and the rainiest day (NULL if it never rained; on ties, the earliest).
| name | region | altitude |
|---|---|---|
| Paris | Ile-de-France | 35 |
| Lyon | Rhone-Alpes | 173 |
| Marseille | Provence | 12 |
| Lille | Hauts-de-France | 20 |
| id | city | day | temp_max | temp_min | rain_mm |
|---|---|---|---|---|---|
| 1 | Paris | 2025-07-01 | 24 | 15 | 0 |
| 2 | Paris | 2025-07-02 | 27 | 17 | 0 |
| 3 | Paris | 2025-07-03 | 22 | 16 | 4.5 |
| 4 | Paris | 2025-07-04 | 19 | 14 | 12 |
| 5 | Paris | 2025-07-05 | 23 | 13 | 0 |
| 6 | Lyon | 2025-07-01 | 26 | 16 | 0 |
| 7 | Lyon | 2025-07-02 | 29 | 18 | 0 |
| 8 | Lyon | 2025-07-03 | 31 | 19 | 0 |
| 9 | Lyon | 2025-07-04 | 24 | 17 | 8.5 |
| 10 | Lyon | 2025-07-05 | 22 | 15 | 3 |
| 11 | Marseille | 2025-07-01 | 30 | 21 | 0 |
| 12 | Marseille | 2025-07-02 | 32 | 22 | 0 |
| 13 | Marseille | 2025-07-03 | 33 | 23 | 0 |
| 14 | Marseille | 2025-07-04 | 29 | 21 | 0 |
| 15 | Marseille | 2025-07-05 | 28 | 20 | 0 |
Show the hint
Topics to use: COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN, CASE, CTEs (WITH), Window functions (OVER).
Query structure:
WITH … AS (
SELECT …, …, …, ROW_NUMBER() OVER (PARTITION BY … ORDER BY … DESC, …) AS …
FROM … …
JOIN … … ON … = …)
SELECT …, SUM(CASE WHEN … > … THEN … ELSE … END), SUM(…), MAX(CASE WHEN … = … AND … > … THEN … END)
FROM …
GROUP BY …Show the solution
WITH r AS (
SELECT c.region, x.day, x.rain_mm, ROW_NUMBER() OVER (PARTITION BY c.region ORDER BY x.rain_mm DESC, x.day) AS rn
FROM cities c
JOIN readings x ON x.city = c.name)
SELECT region, SUM(CASE WHEN rain_mm > 0 THEN 1 ELSE 0 END), SUM(rain_mm), MAX(CASE WHEN rn = 1 AND rain_mm > 0 THEN day END)
FROM r
GROUP BY region;Expected result (3 rows):
| region | SUM(CASE WHEN rain_mm > 0 THEN 1 ELSE 0 END) | SUM(rain_mm) | MAX(CASE WHEN rn = 1 AND rain_mm > 0 THEN day END) |
|---|---|---|---|
| Ile-de-France | 2 | 16.5 | 2025-07-04 |
| Provence | 0 | 0 | NULL |
| Rhone-Alpes | 2 | 11.5 | 2025-07-04 |
Exercise 15
For each account, show the account, the date on which its balance (running total of the transactions by date, then by id) was lowest, and that balance (on ties, the earliest date).
| id | account_id | made_on | amount | label |
|---|---|---|---|---|
| 1 | 1 | 2025-01-02 | 2500 | salary |
| 2 | 1 | 2025-01-05 | -60 | groceries |
| 3 | 1 | 2025-01-12 | -800 | rent |
| 4 | 2 | 2025-01-15 | 500 | transfer |
| 5 | 3 | 2025-01-03 | 1900 | salary |
| 6 | 3 | 2025-01-20 | -120 | groceries |
| 7 | 3 | 2025-02-01 | -950 | rent |
| 8 | 4 | 2025-01-25 | 2100 | salary |
| 9 | 4 | 2025-02-03 | -45 | restaurant |
| 10 | 1 | 2025-02-02 | 2600 | salary |
| 11 | 1 | 2025-02-06 | -75 | groceries |
| 12 | 5 | 2025-02-10 | 30 | interest |
| 13 | 2 | 2025-02-15 | 500 | transfer |
| 14 | 4 | 2025-02-18 | -600 | rent |
Show the hint
Topics to use: WHERE (filters), CTEs (WITH), Window functions (OVER).
Query structure:
WITH … AS (
SELECT …, …, …, SUM(…) OVER (PARTITION BY … ORDER BY …, …) AS …
FROM …), … AS (
SELECT …, …, …, ROW_NUMBER() OVER (PARTITION BY … ORDER BY …, …, …) AS …
FROM …)
SELECT …, …, …
FROM …
WHERE … = …Show the solution
WITH b AS (
SELECT account_id, made_on, id, SUM(amount) OVER (PARTITION BY account_id ORDER BY made_on, id) AS bal
FROM transactions), k AS (
SELECT account_id, made_on, bal, ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY bal, made_on, id) AS rn
FROM b)
SELECT account_id, made_on, bal
FROM k
WHERE rn = 1;Expected result (5 rows):
| account_id | made_on | bal |
|---|---|---|
| 1 | 2025-01-12 | 1640 |
| 2 | 2025-01-15 | 500 |
| 3 | 2025-02-01 | 830 |
| 4 | 2025-02-18 | 1455 |
| 5 | 2025-02-10 | 30 |
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.