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 id | device_idnumber · links to devices | sequence_numbernumber | recorded_atdate & time | received_atdate & time | valuedecimal | battery_pctnumber | gateway_idnumber | signal_rssi_dbmnumber | quality_flagtext |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 17 | 16458 | 2025-03-01 00:00:00 | 2025-03-01 00:00:12 | 18.38 | NULL | NULL | NULL | ok |
| 2 | 7 | 25230 | 2025-03-01 00:00:00 | 2025-03-01 00:00:42 | 5.22 | 67 | 22 | -81 | ok |
| 3 | 169 | 56856 | 2025-03-01 00:02:00 | 2025-03-01 00:02:53 | 40.03 | NULL | NULL | NULL | ok |
| 4 | 12 | 50947 | 2025-03-01 00:03:00 | 2025-03-01 00:03:17 | 36.84 | NULL | NULL | NULL | ok |
| 5 | 34 | 50357 | 2025-03-01 00:02:00 | 2025-03-01 00:03:17 | 40.22 | NULL | NULL | NULL | ok |
| 6 | 123 | 32417 | 2025-03-01 00:03:00 | 2025-03-01 00:03:32 | 38.37 | NULL | NULL | NULL | ok |
- 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.
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.sqlSELECT 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';
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.sqlSELECT 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;
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.sqlSELECT 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';
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.sqlSELECT 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.