SQL exercises with solutions: window functions

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 · level 6

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().

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: 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):

namesalaryROW_NUMBER() OVER (ORDER BY salary DESC, name)
Alice520001
Emma500002
Farid470003
Iris470004
Hugo440005
Jules440006
Bob410007
Claire380008
Gaelle330009
David2900010

Exercise 2 · level 6

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().

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: 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):

namedepartmentROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC, name)
AliceFinance1
HugoFinance2
JulesFinance3
ClaireFinance4
GaelleHR1
DavidHR2
EmmaIT1
FaridIT2
IrisIT3
BobIT4

Exercise 3 · level 6

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.

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
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):

titleratingROUND(rating - AVG(rating) OVER (), 2)
Night Train7.80.6
Blue Harbor7.1-0.1
Paper Moon City6.4-0.8
Silent Peak8.21
Last Signal7.50.3
Summer Keys5.9-1.3
Iron Garden80.8
Dust and Gold6.8-0.4
The Quiet Hour7.40.2
Deep Current6.9-0.3

Exercise 4 · level 6

Show the date of each match and the running number of goals scored in the league up to and including that date (window function).

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
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_onSUM(home_goals + away_goals) OVER (ORDER BY played_on)
2025-08-023
2025-08-033
2025-08-097
2025-08-1011
2025-08-1615
2025-08-1716
2025-08-2317
2025-08-2422
2025-08-3024
2025-08-3126

Exercise 5 · level 6

Show the id, the destination, the price and the average price of the flights to the same destination, rounded to 1 decimal (window function).

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
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):

iddestpriceROUND(AVG(price) OVER (PARTITION BY dest), 1)
6BER140125
10BER110125
4CDG95100
5CDG105100
3FCO7587
7FCO9987
2LIS12095
11LIS7095
9LYS8282
1MAD8977.7
8MAD6577.7
12MAD7977.7

Exercise 6 · level 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().

Table readings (15 rows)
idcitydaytemp_maxtemp_minrain_mm
1Paris2025-07-0124150
2Paris2025-07-0227170
3Paris2025-07-0322164.5
4Paris2025-07-04191412
5Paris2025-07-0523130
6Lyon2025-07-0126160
7Lyon2025-07-0229180
8Lyon2025-07-0331190
9Lyon2025-07-0424178.5
10Lyon2025-07-0522153
11Marseille2025-07-0130210
12Marseille2025-07-0232220
13Marseille2025-07-0333230
14Marseille2025-07-0429210
15Marseille2025-07-0528200
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):

citydayrain_mmLEAD(rain_mm) OVER (PARTITION BY city ORDER BY day)
Lyon2025-07-0100
Lyon2025-07-0200
Lyon2025-07-0308.5
Lyon2025-07-048.53
Lyon2025-07-053NULL
Marseille2025-07-0100
Marseille2025-07-0200
Marseille2025-07-0300
Marseille2025-07-0400
Marseille2025-07-050NULL
Paris2025-07-0100
Paris2025-07-0204.5
Paris2025-07-034.512
Paris2025-07-04120
Paris2025-07-050NULL

Exercise 7 · level 6

For each order that is not cancelled, show its id, its date, its amount and the running revenue (by date, then by id).

Table orders (8 rows)
idcustomer_idorder_datestatus
112025-01-05shipped
222025-01-12shipped
312025-02-03paid
432025-02-10cancelled
542025-02-20shipped
652025-03-02shipped
722025-03-15paid
832025-03-28shipped
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: 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):

idorder_dateamountSUM(amount) OVER (ORDER BY order_date, id)
12025-01-055959
22025-01-12149208
32025-02-0342250
52025-02-2079329
62025-03-0266395
72025-03-15171566
82025-03-28102668

Exercise 8 · level 6

For each transaction, show the account, the date, the amount and the account balance after the transaction (running total by date, then by id).

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: 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_idmade_onamountSUM(amount) OVER (PARTITION BY account_id ORDER BY made_on, id)
12025-01-0225002500
12025-01-05-602440
12025-01-12-8001640
12025-02-0226004240
12025-02-06-754165
22025-01-15500500
22025-02-155001000
32025-01-0319001900
32025-01-20-1201780
32025-02-01-950830
42025-01-2521002100
42025-02-03-452055
42025-02-18-6001455
52025-02-103030

Exercise 9 · level 7

Compute the median salary of all employees (the average of the two middle values when the number of employees is even).

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), 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 · level 7

For each team, show the name of its top scorer (columns: team_id, name).

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: 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_idname
1Alex Moreau
2Carl Weber
3Eli Novak
4Femi Adeyemi
5Hugo Lamy

Exercise 11 · level 7

For each city with at least one dry day, show the city and the longest run of consecutive dry days (rain_mm = 0).

Table readings (15 rows)
idcitydaytemp_maxtemp_minrain_mm
1Paris2025-07-0124150
2Paris2025-07-0227170
3Paris2025-07-0322164.5
4Paris2025-07-04191412
5Paris2025-07-0523130
6Lyon2025-07-0126160
7Lyon2025-07-0229180
8Lyon2025-07-0331190
9Lyon2025-07-0424178.5
10Lyon2025-07-0522153
11Marseille2025-07-0130210
12Marseille2025-07-0232220
13Marseille2025-07-0333230
14Marseille2025-07-0429210
15Marseille2025-07-0528200
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):

cityMAX(n)
Lyon3
Marseille5
Paris2

Exercise 12 · level 8

Give the first month in which the running total of the North region sales exceeds 1000 (a single result).

Table sales (12 rows)
idsellerregionmonthamount
1AnaNorth2025-01300
2AnaNorth2025-02450
3AnaNorth2025-03400
4BenNorth2025-01500
5BenNorth2025-02350
6BenNorth2025-03600
7CleoSouth2025-01200
8CleoSouth2025-02700
9CleoSouth2025-03650
10DanSouth2025-01400
11DanSouth2025-02400
12DanSouth2025-03100
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 · level 8

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.

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: 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):

monthCOUNT(*)SUM(COUNT(*)) OVER (ORDER BY strftime('%Y-%m', loan_date))
2025-0122
2025-0235
2025-0349
2025-04312

Exercise 14 · level 8

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).

Table cities (4 rows)
nameregionaltitude
ParisIle-de-France35
LyonRhone-Alpes173
MarseilleProvence12
LilleHauts-de-France20
Table readings (15 rows)
idcitydaytemp_maxtemp_minrain_mm
1Paris2025-07-0124150
2Paris2025-07-0227170
3Paris2025-07-0322164.5
4Paris2025-07-04191412
5Paris2025-07-0523130
6Lyon2025-07-0126160
7Lyon2025-07-0229180
8Lyon2025-07-0331190
9Lyon2025-07-0424178.5
10Lyon2025-07-0522153
11Marseille2025-07-0130210
12Marseille2025-07-0232220
13Marseille2025-07-0333230
14Marseille2025-07-0429210
15Marseille2025-07-0528200
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):

regionSUM(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-France216.52025-07-04
Provence00NULL
Rhone-Alpes211.52025-07-04

Exercise 15 · level 8

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).

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: 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_idmade_onbal
12025-01-121640
22025-01-15500
32025-02-01830
42025-02-181455
52025-02-1030

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 111 “Window functions (OVER)” questions