The Vital Signs
An easy SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- easy
- Seniority
- L3
- Asked in
- Airbnb
Problem
How many of our health checks against auth-svc in svc_health landed at each status? Nobody enforced casing when status was written, so 'Healthy' and 'healthy' are the same outcome and count together.
Each check records a verdict. See how they stack up.
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
| status | check_count |
|---|---|
| degraded | 5 |
| healthy | 7 |
| timeout | 3 |
| unhealthy | 3 |
Worked solution and explanation
What this is really asking
This is a key-cleaning problem dressed up as an ops tally. Filtering svc_health to auth-svc and counting per status is the part everyone gets right. What separates candidates is noticing that status was written with no casing discipline: 'Healthy' and 'healthy' are one outcome stored two ways, and so are 'DEGRADED' and 'degraded'. Group on the raw string and auth-svc's 18 checks come back as six rows instead of four. Healthy is split 4 and 3 and degraded 3 and 2, so ops reads two half-sized buckets where one real outcome should be.
Build it up
Step 1: Filter on the exact service name
WHERE svc_name = 'auth-svc'. Use equality, not LIKE '%auth%': the table also holds 'edge-auth-svc', a separate service whose four checks would leak into the tally through a substring match.
Step 2: Fold casing inside the grouping key
GROUP BY LOWER(status). The fold has to happen in the key itself, not only in the output column. Lowercasing in the SELECT while grouping on raw status prints tidy labels and still leaves two 'healthy' rows.
Step 3: Count each outcome
COUNT(*) AS check_count. status is never NULL here, so every check lands in a bucket and the four counts sum back to auth-svc's 18 checks. That is a sanity check worth saying out loud.
The solution
SELECT
LOWER(status) AS status,
COUNT(*) AS check_count
FROM svc_health
WHERE svc_name = 'auth-svc'
GROUP BY LOWER(status)SELECT
status,
COUNT(*) AS check_count
FROM svc_health
WHERE svc_name = 'auth-svc'
GROUP BY statusSix rows: 'Healthy' 4 and 'healthy' 3 reported as different outcomes.
- Raw spelling
GROUP BY statusreturns six buckets: 'DEGRADED' 3, 'Healthy' 4, 'degraded' 2, 'healthy' 3, 'timeout' 3, 'unhealthy' 3. Each split outcome looks half as common as it is.- Folded casing
GROUP BY LOWER(status)returns four buckets: 'healthy' 7, 'degraded' 5, 'timeout' 3, 'unhealthy' 3. They still sum to 18, and each outcome appears once.
Common follow-up questions
Common follow-up questions
- Break this down by `region` as well. (Adds `region` to the projection and the grouping; tests whether you keep the casing fold in the key.)
- Restrict to the last 24 hours. (Adds a predicate on `checked`, which finally lets the engine prune partitions.)
- Show each outcome as a share of all auth-svc checks. (Divides each bucket by the total, e.g. `COUNT(*) * 1.0 / SUM(COUNT(*)) OVER ()`, over the folded key.)
- What if a padded spelling like ' healthy ' starts showing up? (Tests whether you extend the fold to `LOWER(TRIM(status))` once padding actually appears in the data.)