API Calls With and Without Errors
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
- L3
Problem
An engineer on the API reliability team is working through an alphabetical checklist of every endpoint in api_calls, judging each call only by whether it logged an error message. Going down that list in order, show each endpoint's clean calls, its calls that produced an error message, and its total call volume.
Some calls succeed. Some do not. Break it down.
Tables
api_calls
Columns: call_id INTEGER, endpoint TEXT, method TEXT, status INTEGER, latency REAL, user_id INTEGER, call_time TEXT, err_msg TEXT
| call_id | endpoint | method | status | latency | user_id | call_time | err_msg |
|---|---|---|---|---|---|---|---|
| 8079 | /api/v1/orders | POST | 200 | 4.2 | 100 | 2026-02-02 01:07:00 | NULL |
| 8158 | /api/v2/products | PUT | 200 | 7.9 | 197 | 2026-03-03 02:14:00 | NULL |
| 8237 | /api/v1/search | DELETE | 201 | 11.6 | 294 | 2026-04-04 03:21:00 | NULL |
| 8316 | /api/v1/auth/login | PATCH | 204 | 15.3 | 391 | 2026-05-05 04:28:00 | NULL |
| 8395 | /api/v1/auth/logout | get | 301 | 19 | NULL | 2026-06-06 05:35:00 | NULL |
| 8474 | /api/v2/analytics | post | 400 | 22.7 | 585 | 2026-07-07 06:42:00 | Not found |
| 8553 | /api/v1/payments | GET | 401 | 26.4 | 682 | 2026-08-08 07:49:00 | Unauthorized |
| 8632 | /api/v1/users/ | POST | 403 | 30.1 | 779 | 2026-09-09 08:56:00 | Rate limit exceeded |
| 8711 | /api/v2/products/ | PUT | 404 | 33.8 | 876 | 2026-01-10 09:03:00 | Internal server error |
| 8790 | /api/v1/users | DELETE | 500 | 37.5 | NULL | 2026-02-11 10:10:00 | Bad gateway |
Expected output
| endpoint | no_error_count | error_count | total_count |
|---|---|---|---|
| /api/v2/products | 0 | 1 | 1 |
| /api/v1/auth/login | 66 | 8 | 74 |
| /api/v1/auth/logout | 50 | 5 | 55 |
| /api/v1/payments | 5 | 7 | 12 |
| /api/v1/search | 82 | 4 | 86 |
Worked solution and explanation
This is a census of a nullable column dressed up as reliability triage. The skill is splitting every endpoint's calls into NULL and non-NULL err_msg buckets in one grouped pass, and reporting both buckets next to the total. Anyone can write GROUP BY endpoint. What separates candidates is remembering that a clean call is stored as NULL, not as an empty string, and that COUNT(err_msg) skips exactly those rows. Fall for it and the clean column either vanishes or quietly reports the error count, on a table where 615 of 670 calls carry no message at all.
Walking to the query
Step 1: Let err_msg decide clean versus errored
The statement counts a call as errored when it produced an error message, so the split lives in err_msg, not in status. A clean call is NULL there, which is what the blank cells in the sample rows are. IS NULL and IS NOT NULL are therefore the two tests, and between them they cover every row exactly once.
Step 2: Count both buckets in one grouped pass
Group by endpoint, turn each test into a 1 or a 0 with CASE, then SUM it. Keep the ELSE 0. Without it the unmatched rows become NULL, and an endpoint with no clean calls at all, like the one-call ' /api/v2/products' line, would report NULL instead of 0.
Step 3: Take the total from COUNT(*)
COUNT(*) counts rows, NULLs and all, so it gives the full call volume. It also gives you a check to say out loud: on every line, no_error_count + error_count must equal total_count, because the two tests partition the rows.
Step 4: Return endpoints in checklist order
ORDER BY endpoint puts the lines in alphabetical order. After grouping, each endpoint value appears once, so the sort key is unique and no two rows can trade places.
The solution
SELECT endpoint,
SUM(CASE WHEN err_msg IS NULL THEN 1 ELSE 0 END) AS no_error_count,
SUM(CASE WHEN err_msg IS NOT NULL THEN 1 ELSE 0 END) AS error_count,
COUNT(*) AS total_count
FROM api_calls
GROUP BY endpoint
ORDER BY endpoint- Counting the column
COUNT(err_msg)skips NULLs, so it returns the errored calls, not the clean ones. Labelledno_error_count, it reports 8 for '/api/v1/auth/login' where the truth is 66. A filter likeerr_msg = ''does worse and matches nothing, because a NULL never equals anything.- Counting a condition
SUM(CASE WHEN err_msg IS NULL THEN 1 ELSE 0 END)counts the NULL rows on purpose, and itsIS NOT NULLtwin counts the rest. Both read the same rows in the same scan, so the two counts always add up toCOUNT(*).
Common follow-up questions
- How would you add an error rate per endpoint without risking a division by zero? (Tests `NULLIF` on the denominator and avoiding integer division.)
- What changes if some services log a missing message as an empty string instead of `NULL`? (Tests folding both encodings into one test, for example `NULLIF(TRIM(err_msg), '')`.)
- The team wants ' /api/v2/products' and '/api/v2/products' on one line. What do you change, and where does that fix belong long term? (Tests normalizing the grouping key and pushing the fix upstream.)
- Break the errored calls down by message, with one column per message type. (Tests extending the conditional aggregate into a crosstab.)
- Restrict it to the last seven days on the 200M-row table. What does the plan do differently? (Tests partition pruning on `call_time`.)