Log Levels

An easy SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
easy
Seniority
L3

Problem

For the weekly incident review, the SRE team is profiling the fast path: log entries that returned in under 300 ms. For each severity level they want the number of these fast entries and their average response time, with the levels logging the most fast entries listed first. Leave out any severity level with fewer than five fast entries.

Severity breakdown with response times.

Tables

server_logs

Columns: log_id INTEGER, server_name TEXT, log_level TEXT, message TEXT, response_time_ms INTEGER, log_timestamp TEXT

server_logs: 10 of 200 rows
log_idserver_namelog_levelmessageresponse_time_mslog_timestamp
137web-prod-02WARNConnection timeout after 30s232026-02-02 01:07:13
174api-prod-01ERRORDatabase query slow: 2.5sNULL2026-03-03 02:14:26
211api-prod-02DEBUGCache miss for key user_123492026-04-04 03:21:39
248db-primaryCRITICALMemory usage at 85%02026-05-05 04:28:52
285db-replica-01INFOSSL certificate expires in 7 daysNULL2026-06-06 05:35:05
322cache-01WARNRate limit exceeded for IP 192.168.1.x882026-07-07 06:42:18
359worker-01ERRORDisk space low: 10% remaining1012026-08-08 07:49:31
396cdn-edge-usDEBUGNew deployment v2.3.1 startedNULL2026-09-09 08:56:44
433cdn-edge-euCRITICALHealth check passed1272026-01-10 09:03:57
470monitor-01INFOFailed to connect to upstream1402026-02-11 10:10:10

Expected output

First 4 of 4 rows
log_levellog_countavg_response_ms
WARN8153
ERROR6187.66666666666666
DEBUG6157.33333333333334
CRITICAL6138.33333333333334

Worked solution and explanation

What this problem is really testing

Beneath the incident-digest costume this is a two-stage filter. First you narrow the rows to the fast path with WHERE response_time_ms < 300, aggregate per log_level, and then you filter the GROUPS by how many fast entries each one logged. The reflex that sinks people is reaching for WHERE to express the at-least-five cutoff, but a row does not know how many siblings share its log_level; that count only exists after GROUP BY, which is exactly what HAVING is for. Put the cutoff in the wrong place and you either cannot write it at all or you drop it and silently keep a level like 'INFO' that only cleared the bar four times.


Build it in stages

Step 1: Narrow to the fast path

Keep only the fast entries with WHERE response_time_ms < 300. This also quietly drops rows where response_time_ms is NULL, since NULL < 300 evaluates to unknown rather than true, so nulls never sneak into the counts.

Step 2: Aggregate per severity level

GROUP BY log_level collapses the fast rows into one row per severity, and per group you emit COUNT(*) AS log_count and AVG(response_time_ms) AS avg_response_ms. AVG skips nulls on its own, though the WHERE has already removed them here.

Step 3: Drop the low-volume levels

HAVING COUNT(*) >= 5 runs after the grouping and removes any level with fewer than five fast entries. On this data that drops 'INFO', which logged only four.

Step 4: Put the busiest levels first

ORDER BY log_count DESC puts the busiest levels first. Here 'CRITICAL', 'DEBUG' and 'ERROR' all sit at six, and the question says nothing about which of them leads, so any order among them is a correct answer. A tiebreaker only earns its place when a LIMIT cuts through a tie and someone has to decide who makes the list.


The solution

Fast-path breakdown by severity
SELECT log_level,
       COUNT(*) AS log_count,
       AVG(response_time_ms) AS avg_response_ms
FROM server_logs
WHERE response_time_ms < 300
GROUP BY log_level
HAVING COUNT(*) >= 5
ORDER BY log_count DESC

Common follow-up questions