Top Regions by Critical Alerts
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
Problem
List the top 5 regions by number of critical alerts in alert_events, highest count first. An alert records only its service, so we place it in a region through that service's checks in svc_health.
Which regions have the highest volume of critical alerts
Tables
alert_events
Columns: alert_id INTEGER, svc_name TEXT, severity TEXT, status TEXT, fired_at TEXT, ack_by TEXT, resolved 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 |
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
| region | critical_count |
|---|---|
| eu-central-1 | 308 |
| eu-west-1 | 288 |
| us-east-1 | 160 |
| us-west-2 | 144 |
Worked solution and explanation
What this really is
This is a dirty-label filter plus a region lookup that lives in the wrong table, dressed up as a postmortem leaderboard. Anyone can write the join and the GROUP BY. What separates candidates is reading the severity sample rows before typing: 'critical' and 'Critical' are both there, and each is about half of the critical alerts. Filter on the lowercase spelling alone and every region's count halves, 'eu-west-1' lands level with 'eu-central-1' at 144, and the top of the list stops meaning anything.
The second decision is the grain. alert_events has no region, so the region has to come from svc_health, which holds many checks per service. 'auth-svc', 'user-svc' and 'gateway' each show up in two regions. The expected counts (308, 288, 160, 144) are per alert-to-check match, not per alert, so reconcile against the preview before you reach for a dedupe.
Step 1: Filter severity across both spellings
WHERE ae.severity IN ('critical', 'Critical') keeps both casings the data holds. LOWER(ae.severity) = 'critical' would also survive a third spelling arriving later, and on this data it matches the same rows. Either way, the filter sits on the alert side so the join only ever sees critical rows.
Step 2: Attach the region from svc_health
Inner-join alert_events to svc_health on svc_name. 'db-primary' fires critical alerts but has no health checks, so it has no region to report under and the inner join drops it. Every critical alert pairs with every check of its service, which is exactly the grain the preview counts.
Step 3: Count the matches per region
GROUP BY sh.region with COUNT(*) AS critical_count counts the joined pairs. 'ap-south-1' never appears: none of the services that both fire critical alerts and have checks is checked there.
Step 4: Cut at five, keeping ties
RANK() OVER (ORDER BY COUNT(*) DESC) numbers the regions by volume, and rnk <= 5 keeps every region tied at the fifth spot instead of letting LIMIT 5 drop one arbitrarily. On this data only four regions have critical alerts, so all four come back, highest count first.
SELECT region, critical_count
FROM (
SELECT
sh.region,
COUNT(*) AS critical_count,
RANK() OVER (ORDER BY COUNT(*) DESC) AS rnk
FROM alert_events ae
INNER JOIN svc_health sh
ON ae.svc_name = sh.svc_name
WHERE ae.severity IN ('critical', 'Critical')
GROUP BY sh.region
) ranked
WHERE rnk <= 5
ORDER BY critical_count DESC;- One spelling
WHERE ae.severity = 'critical'keeps half the critical rows: 'eu-central-1' 144, 'eu-west-1' 144, 'us-east-1' 80, 'us-west-2' 64. The top two tie and every count is off by half.- Both spellings
ae.severity IN ('critical', 'Critical')keeps them all: 'eu-central-1' 308, 'eu-west-1' 288, 'us-east-1' 160, 'us-west-2' 144. That is the result that grades.
Follow-up questions
Common follow-up questions
- How would the query change if each alert should count once per region instead of once per check? (Tests whether you can move the grain deliberately, by deduplicating `svc_health` to (`svc_name`, `region`) or counting distinct `alert_id`.)
- Rewrite this so ties at the fifth spot still return exactly five rows, broken by region name. (Probes `ROW_NUMBER` versus `RANK` and a deterministic tiebreaker in the window's `ORDER BY`.)
- If `region` were NULL for some checks, how should that appear in the output? (Checks whether you treat NULL as its own bucket, a filter or an error, and whether you say which.)
- Same question, but only for alerts fired in the last 24 hours: where does the predicate on `fired_at` go? (Probes filter pushdown on the alert side before the fan-out join.)