Top 2 Busiest API Slots

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
Asked in
Travelport

Problem

A reliability team is triaging which failures crowd the day. Take only the calls that came back with an error message, and read each call's hour as a whole number from 0 to 23: hours before 12 are Morning, hours 12 through 15 are Early Afternoon, and anything later is Late Afternoon. For every HTTP status, count its failures in each slot and return the status-and-slot pairs whose failure count is among the three highest, keeping every pair tied at a count.

Two time slots per week. The busiest.

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 10 rows
statustime_segmentcall_count
401Morning7
404Morning4
500Early Afternoon4
500Late Afternoon4
400Late Afternoon3

Worked solution and explanation

What this really is

Strip the reliability story and this is a tie-preserving top-N over a filtered, two-key bucketized count. The filter is the whole game. Successful calls carry no error message, and on this traffic the success codes (led by status 200, the single hottest status) are the overwhelming majority. Keep them in and you are no longer measuring failures, you are measuring total volume, and the pairs you report become the busiest slots overall. So WHERE err_msg IS NOT NULL is not hygiene, it is the definition of the population.

The second move is the tie rule. Several status-and-slot pairs land on the same count at the bottom of the top three, so DENSE_RANK earns its place over LIMIT 3: LIMIT keeps three rows by an engine-dependent coin flip and silently drops the pairs it did not happen to pick. Get either move wrong and the answer is confidently incorrect: either you are ranking successful traffic, or you are hiding legitimately tied slots.


The three traps

Step 1: The failure filter defines the population

err_msg is empty on the large majority of rows, because those calls succeeded, and status 200 alone dwarfs every error code. Filter on err_msg IS NOT NULL at the source, before the buckets form, and only real failures survive. Drop it and status 200 reappears, floods Morning and both afternoon slots, and the top pairs you return describe successful traffic rather than failures. Filtering after the grouping has the same effect if it lets successes into the count first.

Step 2: Off-by-one on the segment boundaries

The middle slot, '12:00 through 15:00', is inclusive on both ends, so the CASE must read <= 15, not < 15. Write < 15 and hour 15 falls through to the ELSE, lands in 'Late Afternoon', and quietly moves an hour of afternoon failures into the wrong slot. Morning is the easy end (< 12 correctly excludes hour 12); the Early Afternoon upper bound is where people slip. Note the hour has to be read as an integer: CAST(strftime('%H', call_time) AS INTEGER), so the comparison is numeric rather than string.

Step 3: DENSE_RANK vs RANK vs ROW_NUMBER

All three express 'top N' but answer different questions.

- ROW_NUMBER: a fixed number of rows, ties broken arbitrarily. Wrong here. - RANK: skips numbers after a tie, so a wide tie can shove the cutoff past rnk <= 3 unpredictably. - DENSE_RANK: numbers the count tiers with no gaps, so rnk <= 3 reliably means 'every pair in the top three failure counts'.

When pairs tie at the third count, DENSE_RANK returns all of them; that is exactly 'keeping every pair tied at a count'.


The solution

Filter, bucket, dense-rank, keep the top three counts
WITH segmented AS (
  SELECT
    status,
    CASE
      WHEN CAST(strftime('%H', call_time) AS INTEGER) < 12 THEN 'Morning'
      WHEN CAST(strftime('%H', call_time) AS INTEGER) <= 15 THEN 'Early Afternoon'
      ELSE 'Late Afternoon'
    END AS time_segment,
    COUNT(*) AS call_count
  FROM api_calls
  WHERE err_msg IS NOT NULL
  GROUP BY status, time_segment
),
ranked AS (
  SELECT *, DENSE_RANK() OVER (ORDER BY call_count DESC) AS rnk
  FROM segmented
)
SELECT status, time_segment, call_count
FROM ranked
WHERE rnk <= 3

Common follow-up questions