Service Scorecard
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
Services that never fired an alert still belong on the on-call lead's scorecard, with an alert count of zero. For every service with pods in pod_assignments, she wants how many pod assignments it has and how many alerts in alert_events fired for it.
Deploys vs. alerts. One row per service tells the whole story.
Tables
pod_assignments
Columns: assignment_id INTEGER, node_id INTEGER, pod_id INTEGER, svc_name TEXT, assigned_at TEXT, status TEXT
| assignment_id | node_id | pod_id | svc_name | assigned_at | status |
|---|---|---|---|---|---|
| 8047 | 1148 | 2344 | payment-api | 2026-02-02 01:11:00 | draining |
| 8094 | 1259 | 2645 | user-svc | 2026-03-03 02:22:00 | pending |
| 8141 | 1370 | 2946 | search-api | 2026-04-04 03:33:00 | terminated |
| 8188 | 1481 | 3247 | gateway | 2026-05-05 04:44:00 | Active |
| 8235 | 1592 | 3548 | notif-svc | 2026-06-06 05:55:00 | ACTIVE |
| 8282 | 1703 | 3849 | analytics | 2026-07-07 06:06:00 | active |
| 8329 | 1814 | 4150 | ml-serving | 2026-08-08 07:17:00 | draining |
| 8376 | 1925 | 4451 | auth-svc | 2026-09-09 08:28:00 | pending |
| 8423 | 2036 | 4752 | payment-api | 2026-01-10 09:39:00 | terminated |
| 8470 | 2147 | 5053 | user-svc | 2026-02-11 10:50:00 | Active |
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 |
Expected output
| svc_name | assignment_count | alert_count |
|---|---|---|
| analytics | 24 | 0 |
| auth-svc | 24 | 24 |
| gateway | 26 | 26 |
| ml-serving | 24 | 0 |
| payment-api | 26 | 26 |
Worked solution and explanation
This is two independent one-to-many relationships hanging off svc_name, dressed up as an on-call scorecard. A service has dozens of rows in pod_assignments and, if it alerts at all, 24 or 26 in alert_events. The two tables share nothing but the name. Anyone can write the join. What separates candidates is collapsing the assignments to one row per service before the alerts touch them, and counting alert_id rather than rows. Join the raw tables and both columns become assignments times alerts. Count rows and analytics reports an alert it never fired.
Getting to the answer
Step 1: Collapse pod_assignments to one row per service
The assignment_counts CTE groups pod_assignments by svc_name with COUNT(*). After this, a service's assignment total is one number on one row, and nothing downstream can multiply it.
Step 2: Attach alerts to that row with a LEFT JOIN
Joining alert_events to the per-service row emits one row per alert for an alerting service, and one null-padded row for analytics and ml-serving, which have pods but no alerts. An inner join would silently drop those two.
Step 3: Count alert.alert_id, not *
COUNT(alert.alert_id) skips the null on a quiet service's padded row, so it scores 0. Grouping by svc.assignment_count alongside svc.svc_name costs nothing, because the count is constant within a service.
WITH assignment_counts AS (
SELECT svc_name, COUNT(*) AS assignment_count
FROM pod_assignments
GROUP BY svc_name
)
SELECT svc.svc_name, svc.assignment_count, COUNT(alert.alert_id) AS alert_count
FROM assignment_counts svc
LEFT JOIN alert_events alert ON svc.svc_name = alert.svc_name
GROUP BY svc.svc_name, svc.assignment_count- Raw join, one aggregation
- Every assignment row meets every alert row for its service, so
COUNT(*)reports assignments times alerts as the assignment total. There is no way back to either true number from that grain. - Aggregate first, then join
assignment_countsholds one row per service, so the join fans out only over alerts, andassignment_countrides along unchanged.
SELECT asg.svc_name,
COUNT(*) AS assignment_count,
COUNT(alert.alert_id) AS alert_count
FROM pod_assignments asg
LEFT JOIN alert_events alert ON asg.svc_name = alert.svc_name
GROUP BY asg.svc_nameBoth counts are inflated to assignments times alerts for every alerting service.
Common follow-up questions
- Add a third column counting rows in `svc_health` per service. How do you keep all three counts honest? (Pre-aggregating every side to the service grain before combining.)
- The scorecard must also show services that fired alerts but have no pods (`db-primary`, `cache-01`). What changes? (Keeping unmatched rows from both sides and choosing which `svc_name` to report.)
- Only alerts from the last 30 days should count. Where does that filter go so quiet services still appear? (Filtering the alert side in the join condition or a CTE, not in a `WHERE` after the outer join.)