Service Uptime Minutes
A medium SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- medium
- Seniority
- L5
- Asked in
- Samsara
Problem
Find how many full hours each service's still-open alerts in alert_events have been firing as of 2026-09-12, where every alert counts only its own completed hours before they are added up. The reliability team's onboarding review covers alerts that fired no later than 180 days after the service's earliest check in svc_health.
Status changed. How long was it actually up?
Tables
svc_health
Columns: check_id INTEGER, svc_name TEXT, checked TEXT, status TEXT, latency REAL, uptime REAL, region TEXT
| check_id | svc_name | status | latency | uptime | checked | region |
|---|---|---|---|---|---|---|
| 137 | payment-api | degraded | 3.6 | 97.03 | 2026-02-02 01:05:00 | us-west-2 |
| 174 | user-svc | unhealthy | 6.7 | 94.06 | 2026-03-03 02:10:00 | eu-west-1 |
| 211 | search-api | timeout | 9.8 | 91.09 | 2026-04-04 03:15:00 | ap-south-1 |
| 248 | gateway | Healthy | 12.9 | 98.12 | 2026-05-05 04:20:00 | eu-central-1 |
| 285 | notif-svc | DEGRADED | 16 | 95.15 | 2026-06-06 05:25:00 | us-east-1 |
| 322 | ml-serving | healthy | 19.1 | 92.18 | 2026-07-07 06:30:00 | us-west-2 |
| 359 | cache-svc | degraded | NULL | 99.21 | 2026-08-08 07:35:00 | eu-west-1 |
| 396 | queue-worker | unhealthy | 25.3 | 96.24 | 2026-09-09 08:40:00 | ap-south-1 |
| 433 | cron-svc | timeout | 28.4 | 93.27 | 2026-01-10 09:45:00 | eu-central-1 |
| 470 | auth-svc | Healthy | 31.5 | 90.3 | 2026-02-11 10:50:00 | us-east-1 |
alert_events
Columns: alert_id INTEGER, svc_name TEXT, fired_at TEXT, resolved TEXT, severity TEXT, status TEXT, ack_by TEXT
| alert_id | svc_name | severity | status | fired_at | ack_by | resolved |
|---|---|---|---|---|---|---|
| 129 | payment-api | high | acknowledged | 2026-02-02 01:07:00 | bob | 2026-02-02 03:11:00 |
| 158 | user-svc | medium | resolved | 2026-03-03 02:14:00 | charlie | 2026-03-03 04:22:00 |
| 187 | search-api | low | silenced | 2026-04-04 03:21:00 | dana | NULL |
| 216 | gateway | Critical | firing | 2026-05-05 04:28:00 | NULL | 2026-05-05 06:44:00 |
| 245 | notif-svc | HIGH | acknowledged | 2026-06-06 05:35:00 | alice | 2026-06-06 07:55:00 |
| 274 | db-primary | critical | resolved | 2026-07-07 06:42:00 | bob | NULL |
| 303 | cache-01 | high | silenced | 2026-08-08 07:49:00 | charlie | 2026-08-08 09:17:00 |
| 332 | auth-svc | medium | firing | 2026-09-09 08:56:00 | NULL | 2026-09-09 10:28:00 |
| 361 | payment-api | low | acknowledged | 2026-01-10 09:03:00 | eve | NULL |
| 390 | user-svc | Critical | resolved | 2026-02-11 10:10:00 | alice | 2026-02-11 12:50:00 |
Expected output
| svc_name | open_alert_hours |
|---|---|
| auth-svc | 87402 |
| gateway | 57022 |
| notif-svc | 75230 |
| search-api | 91644 |
| user-svc | 65698 |
Worked solution and explanation
What this really is
This is an open-interval measurement dressed up as an uptime report. The missing resolved timestamp is not dirty data to clean away: it IS the open alert, the only row the question is about, and its end point is not in the table at all. Anyone can write the per-service MIN(checked) and the join. What separates candidates is whether muscle memory types resolved IS NOT NULL and measures fired-to-resolved, or drops the test and measures everything to the as-of date. Either way every service's total is wrong, the second one by months of hours from incidents fixed long ago.
Building it
Step 1: Anchor each service on its own first check
The review period starts at each service's earliest row in svc_health, so the svc_start CTE takes MIN(checked) per svc_name. A single fleet-wide start date would let late-onboarded services drag in alerts from well past their first 180 days.
Step 2: Inner-join alerts to the anchor
Join alert_events to svc_start on svc_name. Services that alert but were never health-checked (the seed has a few, like 'db-primary') have no period to measure against, and the inner join drops them on purpose.
Step 3: Keep only the open alerts
a.resolved IS NULL is the whole point of the query. It throws away roughly 94% of the alert rows, every one a closed incident the question does not ask about.
Step 4: Cut at 180 days past the anchor
An open alert has no resolution to test, so the window is on fired_at: keep it when a.fired_at <= datetime(s.first_check, '+180 days'). Alerts raised later in the service's life are outside the onboarding review.
Step 5: Measure to the as-of date, then truncate per alert
julianday('2026-09-12') - julianday(a.fired_at) is days elapsed; times 24 is hours. The CAST to INTEGER sits inside the SUM, so each alert drops its partial hour before the service total is built.
WITH svc_start AS (
SELECT svc_name, MIN(checked) AS first_check
FROM svc_health
GROUP BY svc_name
)
SELECT a.svc_name,
SUM(CAST((julianday('2026-09-12') - julianday(a.fired_at)) * 24 AS INTEGER)) AS open_alert_hours
FROM alert_events a
JOIN svc_start s ON a.svc_name = s.svc_name
WHERE a.resolved IS NULL
AND a.fired_at <= datetime(s.first_check, '+180 days')
GROUP BY a.svc_name- Habit
WHERE resolved IS NOT NULLandresolved - fired_at: a perfectly good closed-incident report, answering a question nobody asked.- The question
WHERE resolved IS NULLand2026-09-12 - fired_at: only the alerts still burning, measured to the moment of the review.
Common follow-up questions
- How would you report closed and open alert hours side by side in one row per service? (Tests splitting one table into two measures on the presence of `resolved`.)
- What if `resolved` is sometimes earlier than `fired_at` because of clock skew? (Probes data-quality handling on timestamp arithmetic.)
- The review runs daily. How would you avoid rescanning all of `svc_health` for the anchor every time? (Tests incremental thinking: the earliest check per service never changes once recorded.)