runtime
43 / 88

lesson 43 Window functions · advanced

Rolling averages & missing dates

Calculate rolling trends while accounting for missing dates.

the idea

A ROWS frame averages a fixed number of neighbouring rows. When readings are missing, “the previous three rows” can span many hours. A RANGE frame over a numeric time value (julianday) averages a fixed time span instead, however many readings it contains.

Dataset: IoT / Sensors

The tables the challenges use, with their first 6 rows. Under each column name: the kind of value it holds, and which table it links to.

sensor_readings10 columns · 150,425 rows
reading_idnumber · unique iddevice_idnumber · links to devicessequence_numbernumberrecorded_atdate & timereceived_atdate & timevaluedecimalbattery_pctnumbergateway_idnumbersignal_rssi_dbmnumberquality_flagtext
117164582025-03-01 00:00:002025-03-01 00:00:1218.38NULLNULLNULLok
27252302025-03-01 00:00:002025-03-01 00:00:425.226722-81ok
3169568562025-03-01 00:02:002025-03-01 00:02:5340.03NULLNULLNULLok
412509472025-03-01 00:03:002025-03-01 00:03:1736.84NULLNULLNULLok
534503572025-03-01 00:02:002025-03-01 00:03:1740.22NULLNULLNULLok
6123324172025-03-01 00:03:002025-03-01 00:03:3238.37NULLNULLNULLok
reading_idnumber · unique id
1
device_idnumber · links to devices
17
sequence_numbernumber
16458
recorded_atdate & time
2025-03-01 00:00:00
received_atdate & time
2025-03-01 00:00:12
valuedecimal
18.38
battery_pctnumber
NULL
gateway_idnumber
NULL
signal_rssi_dbmnumber
NULL
quality_flagtext
ok
reading_idnumber · unique id
2
device_idnumber · links to devices
7
sequence_numbernumber
25230
recorded_atdate & time
2025-03-01 00:00:00
received_atdate & time
2025-03-01 00:00:42
valuedecimal
5.22
battery_pctnumber
67
gateway_idnumber
22
signal_rssi_dbmnumber
-81
quality_flagtext
ok
reading_idnumber · unique id
3
device_idnumber · links to devices
169
sequence_numbernumber
56856
recorded_atdate & time
2025-03-01 00:02:00
received_atdate & time
2025-03-01 00:02:53
valuedecimal
40.03
battery_pctnumber
NULL
gateway_idnumber
NULL
signal_rssi_dbmnumber
NULL
quality_flagtext
ok
reading_idnumber · unique id
4
device_idnumber · links to devices
12
sequence_numbernumber
50947
recorded_atdate & time
2025-03-01 00:03:00
received_atdate & time
2025-03-01 00:03:17
valuedecimal
36.84
battery_pctnumber
NULL
gateway_idnumber
NULL
signal_rssi_dbmnumber
NULL
quality_flagtext
ok
reading_idnumber · unique id
5
device_idnumber · links to devices
34
sequence_numbernumber
50357
recorded_atdate & time
2025-03-01 00:02:00
received_atdate & time
2025-03-01 00:03:17
valuedecimal
40.22
battery_pctnumber
NULL
gateway_idnumber
NULL
signal_rssi_dbmnumber
NULL
quality_flagtext
ok
reading_idnumber · unique id
6
device_idnumber · links to devices
123
sequence_numbernumber
32417
recorded_atdate & time
2025-03-01 00:03:00
received_atdate & time
2025-03-01 00:03:32
valuedecimal
38.37
battery_pctnumber
NULL
gateway_idnumber
NULL
signal_rssi_dbmnumber
NULL
quality_flagtext
ok

first 6 of 150,425 rows · your queries use all of them

worked example

Run these steps in order to see the idea before you write your own query. The step marked “the pattern” is what the challenges ask for. Running the example does not count toward progress.

  1. Smooth with three row neighbors. Run this. Each average uses this reading plus the two previous rows—even if those rows are hours apart.the pattern

    example.sql
    SELECT
      recorded_at,
      value,
      AVG(value) OVER (ORDER BY recorded_at, reading_id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS avg_last_three
    FROM sensor_readings
    WHERE device_id = 2
      AND recorded_at >= '2025-03-10'
      AND recorded_at < '2025-03-11';
  2. Find long gaps between readings. Run this. LAG() measures hours since the prior reading on device 1; the largest values are outages, not normal spacing.

    step-2.sql
    SELECT
      device_id,
      recorded_at,
      ROUND((julianday(recorded_at) - julianday(LAG(recorded_at) OVER (PARTITION BY device_id ORDER BY recorded_at))) * 24, 1) AS hours_since_previous
    FROM sensor_readings
    WHERE device_id = 1
    ORDER BY hours_since_previous DESC
    LIMIT 5;
  3. Count readings in six hours. Run this. A RANGE frame on julianday() counts how many readings fell in the last six hours of clock time, not a fixed row count.

    step-3.sql
    SELECT
      recorded_at,
      value,
      COUNT(*) OVER (ORDER BY julianday(recorded_at) RANGE BETWEEN 0.25 PRECEDING AND CURRENT ROW) AS readings_in_6h
    FROM sensor_readings
    WHERE device_id = 2
      AND recorded_at >= '2025-03-10'
      AND recorded_at < '2025-03-11';
  4. Average over six clock hours. Run this. The same time-based frame averages values from the last six hours only—after a long gap, fewer rows enter the average.

    step-4.sql
    SELECT
      recorded_at,
      value,
      ROUND(AVG(value) OVER (ORDER BY julianday(recorded_at) RANGE BETWEEN 0.25 PRECEDING AND CURRENT ROW), 2) AS avg_6h
    FROM sensor_readings
    WHERE device_id = 2
      AND recorded_at >= '2025-03-10'
      AND recorded_at < '2025-03-11';

common mistake

AVG over ROWS BETWEEN 2 PRECEDING AND CURRENT ROW after a three-hour outage mixes readings from before and after the gap as if they were consecutive.

challenge 1

For device 1 in March 2025, return reading_id, recorded_at, value and avg_3: the average of this and the two previous readings (ordered by recorded_at, reading_id), rounded to 2 decimals.

what to return

Columns
reading_idrecorded_atvalueavg_3 in this order
Rows
any order
challenge-1.sql
 
To master this lesson, solve both challenges without hints and pass the concept check. 3 of 3 to go.
Rolling averages & missing dates — Window functions · Runtime