The Severity Matrix

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

Each row of alert_events is one alert a service fired, and its severity was typed by hand, so the same level turns up in different capitalizations that all mean one level. Produce one row per service with its alert count at each severity level plus its total, busiest services first.

When services cry wolf, the numbers reveal who's serious.

Tables

alert_events

Columns: alert_id INTEGER, svc_name TEXT, severity TEXT, status TEXT, fired_at TEXT, ack_by TEXT, resolved TEXT

alert_events: 10 of 200 rows
alert_idsvc_nameseveritystatusfired_atack_byresolved
129payment-apihighacknowledged2026-02-02 01:07:00bob2026-02-02 03:11:00
158user-svcmediumresolved2026-03-03 02:14:00charlie2026-03-03 04:22:00
187search-apilowsilenced2026-04-04 03:21:00danaNULL
216gatewayCriticalfiring2026-05-05 04:28:00NULL2026-05-05 06:44:00
245notif-svcHIGHacknowledged2026-06-06 05:35:00alice2026-06-06 07:55:00
274db-primarycriticalresolved2026-07-07 06:42:00bobNULL
303cache-01highsilenced2026-08-08 07:49:00charlie2026-08-08 09:17:00
332auth-svcmediumfiring2026-09-09 08:56:00NULL2026-09-09 10:28:00
361payment-apilowacknowledged2026-01-10 09:03:00eveNULL
390user-svcCriticalresolved2026-02-11 10:10:00alice2026-02-11 12:50:00

Expected output

First 5 of 8 rows
svc_namecritical_counthigh_countmedium_countlow_counttotal_count
user-svc16010026
search-api01601026
payment-api0180826
gateway1808026
db-primary1608024

Worked solution and explanation

What this really is

Strip off the on-call costume and this is a pivot with dirty keys: svc_name is the row key, the four severity levels become columns, and every cell is a count. Anyone can write the pivot. What separates candidates is noticing that severity was typed by hand, so 'critical' and 'Critical' are one level, and so are 'high' and 'HIGH'. Compare against the raw value with severity = 'critical' and every capitalized alert falls out of its bucket. The totals still look right, because COUNT(*) never looks at severity. That is exactly why the bug survives review: the level columns quietly stop adding up to the total, and nobody checks.

Step 1: Group by the row key first

GROUP BY svc_name collapses every alert for a service into one output row, so each expression in the SELECT becomes an aggregate over that service's slice. The dimension you group on becomes the rows; the conditions you write become the columns.

Step 2: Fold the casing, then carve each level

Wrap severity in LOWER before comparing, so 'Critical' and 'HIGH' land with their lowercase twins. Each CASE returns 1 when the folded value matches its level and 0 otherwise. Every CASE fires only on its own value, so the columns stay independent.

Step 3: Sum the flags, do not count them

SUM rolls the 1s and 0s into each service's count for that level. COUNT would be wrong here: it tallies every non-NULL value, and the 0 from the ELSE branch is non-NULL, so COUNT(CASE WHEN ... THEN 1 ELSE 0 END) returns the full alert total in every column. Use SUM with ELSE 0, or COUNT with no ELSE.

Step 4: Total, then busiest first

COUNT(*) gives each service's total regardless of severity and doubles as a sanity check: the four buckets must add up to it, and on this data they do only once the casing is folded. Sort by total_count descending. Several services tie on the same total, but the review only needs the busiest on top, so no further sort key is needed.

Case-folded conditional aggregation pivot
SELECT svc_name,
       SUM(CASE WHEN LOWER(severity) = 'critical' THEN 1 ELSE 0 END) AS critical_count,
       SUM(CASE WHEN LOWER(severity) = 'high' THEN 1 ELSE 0 END) AS high_count,
       SUM(CASE WHEN LOWER(severity) = 'medium' THEN 1 ELSE 0 END) AS medium_count,
       SUM(CASE WHEN LOWER(severity) = 'low' THEN 1 ELSE 0 END) AS low_count,
       COUNT(*) AS total_count
FROM alert_events
GROUP BY svc_name
ORDER BY total_count DESC

One pass, one GROUP BY, each level carved out by a case-folded CASE expression.

Case-sensitive: `severity = 'critical'`
Matches only the lowercase spelling. 'Critical' rows fall through every bucket and surface only in total_count, so critical_count reads far lower than reality.
Case-folded: `LOWER(severity) = 'critical'`
Every capitalization of the level lands in one bucket, so critical_count is the true number of critical alerts and the four buckets sum to total_count.

Common follow-up questions