Where Did the Quiet Day Go?
Define UTC timestamps, half-open reporting intervals and a calendar spine so missing days do not disappear or midnight events get counted twice.
14 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Event Logs and Calendars Answer Different Questions
BOTIE
A daily report needs more than a timestamp function. It needs a set of expected days. Joining a calendar to an event log preserves days that have no matching event, just as an asset register preserves pumps that have no visit.
Sources for this history
The Report Has Skipped Tuesday
Botie groups recorded incidents by day and draws a chart. Monday has two incidents and Wednesday has one. Tuesday disappears entirely, which makes the chart look as though Wednesday immediately followed Monday.
The event table records things that happened. It cannot supply a row for a day when nothing happened. We need a calendar to define the days we intend to report, plus a precise rule for assigning each event to one day.
The records are invented maintenance examples. The workshop exposes record operations; the SQL worksheet executes a real query in an isolated SQLite database.
Step 1 of 1
The calendar has a row the event log cannot supply
The requested report includes January 1, 2 and 3.
| UTC day | Recorded events | Required count |
|---|---|---|
| 2026-01-01 | 08:00:00 and 23:59:59 | 2 |
| 2026-01-02 | None | 0 |
| 2026-01-03 | 00:00:00 | 1 |
2026-01-01
- Recorded events
- 08:00:00 and 23:59:59
- Required count
- 2
2026-01-02
- Recorded events
- None
- Required count
- 0
2026-01-03
- Recorded events
- 00:00:00
- Required count
- 1
The calendar defines the report’s rows
The calendar preserves report rows before the deliberate zero-fill.
Keep every requested calendar day
Read each event’s UTC day, count events by day, and left join those counts onto the requested calendar.
BOTIE
Calendar days with event counts
[
{
"day": "2026-01-01",
"n": 2
},
{
"day": "2026-01-02",
"n": 0
},
{
"day": "2026-01-03",
"n": 1
}
]Follow the operations
1. Requested report days
[ { "day": "2026-01-01" }, { "day": "2026-01-02" }, { "day": "2026-01-03" } ]2. Recorded UTC events
[ { "occurred_at": "2026-01-01 08:00:00" }, { "occurred_at": "2026-01-01 23:59:59" }, { "occurred_at": "2026-01-03 00:00:00" } ]3. Read the UTC calendar day
[ { "occurred_at": "2026-01-01 08:00:00", "day": "2026-01-01" }, { "occurred_at": "2026-01-01 23:59:59", "day": "2026-01-01" }, { "occurred_at": "2026-01-03 00:00:00", "day": "2026-01-03" } ]4. Summarise each group
[ { "day": "2026-01-01", "n": 2 }, { "day": "2026-01-03", "n": 1 } ]5. Match rows on a key
[ { "day": "2026-01-01", "n": 2 }, { "day": "2026-01-02", "n": null }, { "day": "2026-01-03", "n": 1 } ]6. Set a constant
0
7. Replace a missing result deliberately
[ { "day": "2026-01-01", "n": 2 }, { "day": "2026-01-02", "n": 0 }, { "day": "2026-01-03", "n": 1 } ]8. Choose table columns
[ { "day": "2026-01-01", "n": 2 }, { "day": "2026-01-02", "n": 0 }, { "day": "2026-01-03", "n": 1 } ]9. Order rows by named fields
[ { "day": "2026-01-01", "n": 2 }, { "day": "2026-01-02", "n": 0 }, { "day": "2026-01-03", "n": 1 } ]10. Result
[ { "day": "2026-01-01", "n": 2 }, { "day": "2026-01-02", "n": 0 }, { "day": "2026-01-03", "n": 1 } ]
The workshop accepts valid fixed-format UTC timestamps. It does not infer site timezones or distinguish real quiet days from ingestion outages.
1. Agree on the timestamp representation
These fixtures store UTC timestamps as fixed-width YYYY-MM-DD HH:MM:SS text. In that one representation, text order follows chronological order. Mixed offsets, inconsistent widths or a mixture of seconds and milliseconds would invalidate that shortcut.
Store or validate the format at ingestion, and document the unit for numeric timestamps. A column named occurred_at does not by itself guarantee a timezone or a unit.
SQLite’s date functions support particular representations. They are not a substitute for deciding what the source field means.
2. Give adjacent days boundaries that do not overlap
We assign an event to a day when its timestamp is at least the start of that day and strictly less than the start of the next day. This half-open interval includes the starting midnight and excludes the next midnight.
BETWEEN includes both endpoints. Using it with the two midnights would count a boundary event in both adjacent days. Guessing a final instant such as 23:59:59 also ties the query to a precision that may change.
Use the next interval’s start as the exclusive upper bound.
Inspect January 1’s exact interval
The event at midnight on January 3 lies outside this first-day interval.
| day |
|---|
| 2026-01-01 |
| 2026-01-02 |
| 2026-01-03 |
| event_id | occurred_at |
|---|---|
| 1 | 2026-01-01 08:00:00 |
| 2 | 2026-01-01 23:59:59 |
| 3 | 2026-01-03 00:00:00 |
Database setup for this example
CREATE TABLE calendar(day TEXT PRIMARY KEY NOT NULL);
CREATE TABLE events(event_id INTEGER PRIMARY KEY, occurred_at TEXT NOT NULL);
INSERT INTO calendar VALUES ('2026-01-01'),('2026-01-02'),('2026-01-03');
INSERT INTO events VALUES (1,'2026-01-01 08:00:00'),(2,'2026-01-01 23:59:59'),(3,'2026-01-03 00:00:00');SELECT event_id FROM events
WHERE occurred_at >= '2026-01-01 00:00:00'
AND occurred_at < '2026-01-02 00:00:00'
ORDER BY event_id;| event_id |
|---|
| 1 |
| 2 |
The two January 1 events qualify without guessing the final supported fractional second.
3. Put the calendar on the preserved side of a left join
The calendar table lists the days we want, including those with no event. LEFT JOIN keeps those calendar rows. Put the event-time conditions in ON so an unmatched day still survives.
Count the non-NULL event_id, not COUNT(*). A left join supplies one placeholder row for an unmatched day; COUNT(*) would incorrectly turn that placeholder into one incident.
The fixture’s event_id is always known on a real event, so it is a reliable counted match.
Inspect the calendar’s unmatched placeholder
Before counting, inspect which event IDs were attached to each requested day.
| day |
|---|
| 2026-01-01 |
| 2026-01-02 |
| 2026-01-03 |
| event_id | occurred_at |
|---|---|
| 1 | 2026-01-01 08:00:00 |
| 2 | 2026-01-01 23:59:59 |
| 3 | 2026-01-03 00:00:00 |
Database setup for this example
CREATE TABLE calendar(day TEXT PRIMARY KEY NOT NULL);
CREATE TABLE events(event_id INTEGER PRIMARY KEY, occurred_at TEXT NOT NULL);
INSERT INTO calendar VALUES ('2026-01-01'),('2026-01-02'),('2026-01-03');
INSERT INTO events VALUES (1,'2026-01-01 08:00:00'),(2,'2026-01-01 23:59:59'),(3,'2026-01-03 00:00:00');SELECT c.day, e.event_id
FROM calendar AS c LEFT JOIN events AS e
ON e.occurred_at >= c.day || ' 00:00:00'
AND e.occurred_at < datetime(c.day, '+1 day')
ORDER BY c.day, e.event_id;| day | event_id |
|---|---|
| 2026-01-01 | 1 |
| 2026-01-01 | 2 |
| 2026-01-02 | NULL |
| 2026-01-03 | 3 |
January 2 survives with no event ID. The challenge turns these matches into daily counts without counting that placeholder as an incident.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which daily interval should include it?
Use the information below.
The interval is [2026-01-01 00:00:00, 2026-01-02 00:00:00). An event occurs exactly at the upper boundary.4. Decide when a missing count really means zero
A calendar left-joined to precomputed daily counts also works. In that arrangement, an absent count is NULL and COALESCE can deliberately convert it to zero. That is the arrangement used by the workshop.
This assumes the log is complete enough for “no recorded events” to be useful. If the sensor system was offline, zero records does not prove zero real incidents. Keep ingestion coverage or service status separately when the distinction matters.
Replacing missing data with zero is a statement about the report’s meaning, not a general cleanup rule.
5. Keep UTC reporting separate from local business days
Our day boundary is midnight UTC. A maintenance team may instead ask for days in a named timezone or shifts beginning at 07:00. Those are different boundaries and can cross UTC dates.
Use a timezone-aware system to construct the intended interval boundaries. Daylight-saving changes can make a local day shorter or longer than twenty-four hours. SQLite’s host-local conversion is not a general named-timezone database for arbitrary sites.
Once the correct UTC boundaries are known, the same inclusive-start/exclusive-end comparison can assign events consistently.
6. Measure elapsed time using numeric instants
For elapsed seconds in this second-precision fixture, subtract unixepoch values. A calendar label alone is not an elapsed duration, and months are not a fixed number of seconds.
For fractional precision, choose a representation and conversion that preserve the required resolution. Functions available in newer SQLite releases may not exist in the browser’s runtime; these examples use functions supported by the lesson environment.
Round only for display when the underlying precision matters.
Measure a ninety-minute repair
Both inputs explicitly use UTC, so timezone transitions do not change this elapsed interval.
Database setup for this example
SELECT unixepoch('2026-01-03 09:30:00') - unixepoch('2026-01-03 08:00:00') AS seconds;| seconds |
|---|
| 5400 |
The numeric difference is elapsed seconds, rather than a formatted clock time.
7. Test the beginning, the boundary and the empty interval
Insert an event exactly at the next midnight. It must enter the next day only. Add an event outside the requested calendar range and verify that no extra report day appears. Remove every event and verify that every requested calendar day still appears with zero.
These cases catch reporting mistakes that a busy day full of ordinary timestamps can hide.
The requested calendar is part of the query input. Do not derive it only from days that happened to have records.
Check yourself
Questions on Parts 1 to 7
Question 1 of 2
What does a count of zero establish?
Use the information below.
The event log has no records for a day when the ingestion service was offline.Try it with Botie
Build with Botie
Write and run SQL