What Set It Off
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
- L4
Problem
Each row of ci_builds is one CI build, labeled with the trigger that kicked it off and how long it ran in seconds. Show how many builds each trigger produced and their average duration, listed alphabetically by trigger.
Every build has a cause. Tally the work behind each one.
Tables
ci_builds
Columns: build_id INTEGER, repo_name TEXT, branch TEXT, status TEXT, dur_secs INTEGER, trigger TEXT, built_at TEXT
| build_id | repo_name | branch | status | dur_secs | trigger | built_at |
|---|---|---|---|---|---|---|
| 5047 | backend-api | develop | failed | 34 | pull_request | 2026-02-02 01:13:00 |
| 5094 | infra | feature/auth | canceled | 53 | schedule | 2026-03-03 02:26:00 |
| 5141 | ml-pipeline | fix/login-bug | running | 72 | manual | 2026-04-04 03:39:00 |
| 5188 | mobile-app | release/v2.0 | queued | 91 | tag | 2026-05-05 04:52:00 |
| 5235 | data-platform | hotfix/payment | success | 110 | webhook | 2026-06-06 05:05:00 |
| 5282 | frontend | feature /spaces | failed | NULL | push | 2026-07-07 06:18:00 |
| 5329 | backend-api | main | canceled | 148 | pull_request | 2026-08-08 07:31:00 |
| 5376 | infra | develop | running | 167 | schedule | 2026-09-09 08:44:00 |
| 5423 | ml-pipeline | feature/auth | queued | 186 | manual | 2026-01-10 09:57:00 |
| 5470 | mobile-app | fix/login-bug | success | 205 | tag | 2026-02-11 10:10:00 |
Expected output
| trigger | build_count | avg_duration |
|---|---|---|
| manual | 34 | 454.5882352941176 |
| pull_request | 34 | 416.5882352941176 |
| push | 32 | NULL |
| schedule | 34 | 435.5882352941176 |
| webhook | 32 | 458.75 |
Worked solution and explanation
What this is really testing
This looks like a CI reporting task, but underneath it is one grouped tally. Its only real question is how you treat a build that never recorded a duration. On this data every 'push' build has a null dur_secs: 32 builds, and not one timing among them. The count for 'push' must still say 32, and its average has to come back null, not 0. Write COUNT(dur_secs) for the count and 'push' reports zero builds. Hand-roll the average and you invite a wrong denominator or a truncated one.
SELECT
trigger,
COUNT(*) AS build_count,
AVG(dur_secs) AS avg_duration
FROM ci_builds
GROUP BY trigger
ORDER BY trigger;One pass, grouped by trigger, with AVG doing the null handling for free.
- `AVG(dur_secs)`
- The denominator is the number of builds that recorded a duration. A trigger with no timings returns null, which is exactly what 'push' shows.
- `SUM(dur_secs) / COUNT(*)`
- The denominator is every row in the group. Because
dur_secsis an integer, many engines do integer division here, so 'manual' reads 454 instead of 454.588. On any trigger that mixes timed and untimed builds, the nulls also sit in the denominator and drag the average toward zero.
Step 1: Pick the grain
One row per trigger means GROUP BY trigger. Every column in the select list is then either the grouping key or an aggregate over that group.
Step 2: Count rows, not durations
COUNT(*) counts rows regardless of nulls, so all 32 untimed 'push' builds land in build_count. That is deliberate: the builds happened even though nobody recorded how long they took.
Step 3: Order for the reader
trigger is unique per output row, so sorting on it alphabetically gives one fixed order a reviewer can scan top to bottom.
Common follow-up questions
- How would you also report, per trigger, the number of builds that never recorded a duration? (Tests `COUNT(*)` minus `COUNT(dur_secs)`, or a conditional sum over `dur_secs IS NULL`.)
- Now restrict the report to builds from the current year. How does that change the query? (Tests a year filter on `built_at` applied before the grouping.)