Services at Median Uptime
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
- L4
- Asked in
- General Assembly
Problem
Which services land in the two weakest of four equal tiers when the fleet is split by each service's average uptime across all of its health checks in svc_health? For each one, the SRE team's triage sheet needs that average, how far it sits below the fleet's most reliable service, and how far below the strongest service in its own tier.
Exactly at the median. Not above, not below.
Tables
svc_health
Columns: check_id INTEGER, svc_name TEXT, status TEXT, latency REAL, uptime REAL, checked TEXT, 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 |
Expected output
| svc_name | avg_uptime | gap_to_best | gap_to_tier_best |
|---|---|---|---|
| auth-svc | 91.68529411764706 | 5.841580882352943 | 1.8694117647058874 |
| cron-svc | 93.55470588235295 | 3.9721691176470557 | 0 |
| ml-serving | 93.410625 | 4.116250000000008 | 0.14408088235295224 |
| notif-svc | 94.99705882352941 | 2.5298161764705895 | 0.5947058823529403 |
| payment-api | 95.310625 | 2.2162500000000023 | 0.28113970588235304 |
Worked solution and explanation
What this really is
Beneath the on-call story this is a population-relative bucketing problem with two reference comparisons bolted on. First you collapse a firehose of health checks into one uptime number per service, then you cut the services into four population-sized tiers and keep the weakest half. Then each survivor gets two yardsticks: how far it trails the single best service in the whole fleet, and how far it trails the best service in its own tier. Almost everyone reaches for the tiering idea. Where they fall is grain: bucket the raw check rows and a service polled every minute floods the low tiers while a quiet one vanishes, so you end up tiering monitoring volume instead of reliability.
Build it in four moves
Step 1: Collapse to one uptime per service
The table is a stream of individual checks, but reliability is a property of the service. Average uptime per svc_name first, so every service is exactly one member of the population. Skip this and you tier raw events, and the noisiest service wins.
Step 2: Cut the services into four tiers, keep the weakest two
NTILE(4) OVER (ORDER BY avg_uptime) hands each service a tier from 1 (lowest uptime) to 4. Keeping tiers 1 and 2 with WHERE a.reliability_tier <= 2 is the below-median half, and because NTILE works off the ordered set it never needs a hardcoded count.
Step 3: Self-join on the tier to find each tier's leader
The tier leader is a per-group reference a single global MAX cannot give you. Join tiered to itself on equal reliability_tier, so each service sees only its own tier's members, then MAX(b.avg_uptime) is that tier's ceiling. Subtract the service's own average and you have gap_to_tier_best.
Step 4: Measure the gap to the fleet best
The fleet leader is global, so the scalar subquery (SELECT MAX(avg_uptime) FROM tiered) pins it once, and each survivor's shortfall from it is gap_to_best. It reads over all of tiered, not just the weak half, which is exactly why it cannot share the self-join's scope.
The solution
WITH svc_uptime AS (
SELECT svc_name, AVG(uptime) AS avg_uptime
FROM svc_health
GROUP BY svc_name
),
tiered AS (
SELECT svc_name, avg_uptime,
NTILE(4) OVER (ORDER BY avg_uptime) AS reliability_tier
FROM svc_uptime
)
SELECT a.svc_name,
a.avg_uptime,
(SELECT MAX(avg_uptime) FROM tiered) - a.avg_uptime AS gap_to_best,
MAX(b.avg_uptime) - a.avg_uptime AS gap_to_tier_best
FROM tiered a
JOIN tiered b ON b.reliability_tier = a.reliability_tier
WHERE a.reliability_tier <= 2
GROUP BY a.svc_name, a.avg_uptime- Bucketing raw check rows (wrong grain)
NTILEover the raw checks tiers individual readings. A service polled every minute contributes thousands of rows and drowns the low tiers; a rarely checked service barely registers. The output is a list of checks, not services.- Bucketing services (correct grain)
GROUP BY svc_namefirst, so each service is one member of the population.NTILEthen tiers the services evenly and the weakest half is a genuine set of low-uptime services.
Common follow-up questions
- The team now wants the weakest decile instead of the weakest half. What changes? (Tests whether `NTILE(n)` generalizes and the tier filter adjusts without restructuring.)
- Some `uptime` readings are `NULL`. How does that move a service's average and its tier? (Probes `NULL` handling in `AVG` over `svc_health.uptime` and whether a mostly-`NULL` service should tier at all.)
- How would you weight recent checks more heavily than old ones when computing each service's uptime? (Tests grain and weighting: a raw average versus a recency-aware measure of reliability built from `checked`.)