Days off between shifts
driver_days has one row per day a driver was online. A missing day is a day off.
Ops wants to see every break: the last day a driver was online before it, the day they came back, and how many days they were off in between.
Return one row per break with:
| column | type | meaning |
|---|---|---|
driver_id |
integer | the driver |
last_online |
date | the online day just before the break |
back_online |
date | the first online day after the break |
days_off |
integer | days between the two, not counting either end |
Online on the 3rd and back on the 8th is a break of 4 days off (the 4th to the 7th).
Edge cases:
- Two online days in a row are not a break. Leave them out.
- Days off before a driver’s first online day or after their last don’t count. There’s nothing on both sides.
- The same day can appear twice for a driver. Treat it as one online day.
Row order doesn’t matter.
Tables: driver_days.
ridelog
A ride-hailing app across five cities. Synthetic data, generated by scripts/gen-datasets.mjs.
Click a table to preview its first 10 rows.
drivers 50 rows
50 drivers.
trips 2,000 rows
2,000 trips over 60 days from 2025-03-01. Cancelled trips have NULL ended_at, distance_km and fare.
events 3,146 rows
app events for 120 users. About 5% were ingested twice: same user_id, event_type, event_ts, different event_id and later ingested_at.
driver_days 578 rows
one row per day a driver was online (first 20 drivers, 60 days). Gaps are days off.
Hints get more specific as you go. Reveal one, try again, then come back.
A break sits between two online days that aren’t next to each other. So for each online day you need the online day just before it, for the same driver.
LAG(active_date) OVER (PARTITION BY driver_id ORDER BY active_date) puts the previous online day on
the same row.
In DuckDB, subtracting two dates gives a whole number of days. Keep rows where
back_online - last_online > 1, and days_off is that difference minus one. A repeated day gives a
difference of 0, so the same filter drops it.
Try one more hint first? The solution is always here, but working it out is what makes it stick.
Approach
Sort each driver’s online days, look one row back, and keep the pairs that aren’t adjacent.
WITH ordered AS (
SELECT driver_id,
LAG(active_date) OVER (PARTITION BY driver_id ORDER BY active_date) AS last_online,
active_date AS back_online
FROM driver_days
)
SELECT driver_id,
last_online,
back_online,
back_online - last_online - 1 AS days_off
FROM ordered
WHERE back_online - last_online > 1;
Why it works
After sorting, every gap in the calendar shows up as a jump between two neighbouring rows. LAG
brings the neighbour onto the current row, so the jump is one subtraction.
The filter does three jobs at once. Consecutive days give a difference of 1 and drop out. A repeated
day gives 0 and drops out. A driver’s first row has no previous day, so LAG returns NULL, the
comparison is NULL, and WHERE drops it too. That’s why days off before the first shift never
appear.
The filter has to sit outside the CTE. Window functions run after WHERE, so you can’t filter on
LAG(...) in the same SELECT that computes it.
Under the hood
DuckDB hash-partitions the rows by driver_id, sorts each partition by active_date, and then walks
each partition once. LAG with no frame clause is just “the value one row up”, so the walk is linear
after the sort. The sort is the expensive part: roughly n log n over 578 rows here.
Per-problem discussion threads are coming. Until then, questions and better solutions are welcome on LinkedIn or by email at hello@builtfrombits.com.
Press ⌘ ↵ or Run to check the visible tests. Submit runs the hidden ones too. Everything runs in your browser.