The Merge Counter

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

Problem

Each row of ci_builds is one CI run, and any run whose trigger is something other than 'manual' counts as a merge build. For 2026, put the three long-lived branches main, develop and release side by side: how many merge builds each had, expressed both as a whole number and as a decimal, plus their average build duration, with a branch that had no merge builds still shown at zero.

How many builds came from merges?

Tables

ci_builds

Columns: build_id INTEGER, repo_name TEXT, branch TEXT, status TEXT, dur_secs INTEGER, trigger TEXT, built_at TEXT

ci_builds: 10 of 200 rows
build_idrepo_namebranchstatusdur_secstriggerbuilt_at
5047backend-apidevelopfailed34pull_request2026-02-02 01:13:00
5094infrafeature/authcanceled53schedule2026-03-03 02:26:00
5141ml-pipelinefix/login-bugrunning72manual2026-04-04 03:39:00
5188mobile-apprelease/v2.0queued91tag2026-05-05 04:52:00
5235data-platformhotfix/paymentsuccess110webhook2026-06-06 05:05:00
5282frontendfeature /spacesfailedNULLpush2026-07-07 06:18:00
5329backend-apimaincanceled148pull_request2026-08-08 07:31:00
5376infradeveloprunning167schedule2026-09-09 08:44:00
5423ml-pipelinefeature/authqueued186manual2026-01-10 09:57:00
5470mobile-appfix/login-bugsuccess205tag2026-02-11 10:10:00

Expected output

First 3 of 3 rows
branchbuild_countbuild_count_decimalavg_duration
develop1414440
main1414412.5
release00NULL

Worked solution and explanation

What this really is

This is a fixed-row report dressed up as a branch comparison. The report owes you exactly three rows, main, develop and release, and the seed has no build on a branch named exactly 'release' at all. Anyone can filter to merge builds and count them. What separates candidates is noticing that the row list comes from the question, not from the data. Let the data decide which rows exist and 'release' quietly disappears, so the comparison ships with a hole where its zero should be.

Grouping by branch
GROUP BY branch only creates a group for a value that survives the WHERE. No row has branch = 'release', so the result has two rows, main and develop, and the third branch is simply gone.
One aggregate per named branch
An aggregate with no grouping always returns exactly one row. Over zero rows that row is COUNT(*) = 0 and AVG(dur_secs) is NULL, so 'release' shows up at zero, labelled by the literal you gave it.

Step 1: Define a merge build once

A merge build is any run whose trigger is not 'manual'. trigger has no NULLs in ci_builds, so trigger != 'manual' is the whole rule here. On a feed where it could be NULL you would decide on purpose whether those runs count, because NULL != 'manual' is not true and drops the row.

Step 2: Scope to 2026 by when the run happened

built_at spans five years. strftime('%Y', built_at) = '2026' keeps only this year's runs. Leave it out and main jumps from 14 merge builds to 24, which is the kind of quiet inflation nobody catches in review.

Step 3: Name each branch as a literal

Each branch gets its own SELECT whose WHERE ends in branch = '...' and whose output column is the literal name, not the branch column. That literal is what lets the empty 'release' select still label its row: there is no branch value to read, only the zero-row aggregate. Exact equality also keeps 'release/v2.0' out of 'release'.

Step 4: Stitch the three rows together

Each select returns exactly one row, so combining the three with UNION gives exactly three. The literal names differ, so the duplicate check never removes anything; UNION ALL would return the same rows and skip that check.

The solution

One scalar aggregate per branch
SELECT 'main' AS branch,
       COUNT(*) AS build_count,
       CAST(COUNT(*) AS REAL) AS build_count_decimal,
       AVG(dur_secs) AS avg_duration
FROM ci_builds
WHERE trigger != 'manual'
  AND strftime('%Y', built_at) = '2026'
  AND branch = 'main'
UNION
SELECT 'develop' AS branch,
       COUNT(*) AS build_count,
       CAST(COUNT(*) AS REAL) AS build_count_decimal,
       AVG(dur_secs) AS avg_duration
FROM ci_builds
WHERE trigger != 'manual'
  AND strftime('%Y', built_at) = '2026'
  AND branch = 'develop'
UNION
SELECT 'release' AS branch,
       COUNT(*) AS build_count,
       CAST(COUNT(*) AS REAL) AS build_count_decimal,
       AVG(dur_secs) AS avg_duration
FROM ci_builds
WHERE trigger != 'manual'
  AND strftime('%Y', built_at) = '2026'
  AND branch = 'release'
The version that loses a branch
SELECT branch,
       COUNT(*) AS build_count,
       CAST(COUNT(*) AS REAL) AS build_count_decimal,
       AVG(dur_secs) AS avg_duration
FROM ci_builds
WHERE trigger != 'manual'
  AND strftime('%Y', built_at) = '2026'
  AND branch IN ('main', 'develop', 'release')
GROUP BY branch

Returns main and develop only: no row survives the filter for 'release', so no group is ever made for it.

Common follow-up questions