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

api_calls: 10 of 670 rows
call_idendpointmethodstatuslatencyuser_idcall_timeerr_msg
8079/api/v1/ordersPOST2004.21002026-02-02 01:07:00NULL
8158/api/v2/productsPUT2007.91972026-03-03 02:14:00NULL
8237/api/v1/searchDELETE20111.62942026-04-04 03:21:00NULL
8316/api/v1/auth/loginPATCH20415.33912026-05-05 04:28:00NULL
8395/api/v1/auth/logoutget30119NULL2026-06-06 05:35:00NULL
8474/api/v2/analyticspost40022.75852026-07-07 06:42:00Not found
8553/api/v1/paymentsGET40126.46822026-08-08 07:49:00Unauthorized
8632/api/v1/users/POST40330.17792026-09-09 08:56:00Rate limit exceeded
8711/api/v2/products/PUT40433.88762026-01-10 09:03:00Internal server error
8790/api/v1/usersDELETE50037.5NULL2026-02-11 10:10:00Bad gateway

Expected output

First 5 of 12 rows
endpointno_error_counterror_counttotal_count
/api/v2/products011
/api/v1/auth/login66874
/api/v1/auth/logout50555
/api/v1/payments5712
/api/v1/search82486

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

Case-branch for api calls without errors
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. Labelled no_error_count, it reports 8 for '/api/v1/auth/login' where the truth is 66. A filter like err_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 its IS NOT NULL twin counts the rest. Both read the same rows in the same scan, so the two counts always add up to COUNT(*).

Common follow-up questions