Annual Pipeline Failures

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
Asked in
City and County of San Francisco

Problem

Run status in data_pipes is not written consistently, so the same outcome can show up in different letter case. Count the failed runs per year for the annual reliability review, looking only at runs that recorded a runtime, and list the years from earliest to latest.

How many pipelines broke this year?

Tables

data_pipes

Columns: pipe_id INTEGER, pipe_name TEXT, status TEXT, rows_in INTEGER, rows_out INTEGER, start_at TEXT, dur_secs INTEGER

data_pipes: 10 of 200 rows
pipe_idpipe_namestatusrows_inrows_outstart_atdur_secs
131etl_usersfailed137972026-02-02 01:00:0028
162etl_eventsrunning2741942026-03-03 02:00:0051
193sync_crmqueued4112912026-04-04 03:00:0074
224agg_dailyskipped5483882026-05-05 04:00:0097
255agg_weeklySuccess6854852026-06-06 05:00:00NULL
286export_s3FAILED8225822026-07-07 06:00:00143
317import_csvsuccessNULL6792026-08-08 07:00:00166
348ml_trainfailed10967762026-09-09 08:00:00189
379dbt_buildrunning1233NULL2026-01-10 09:00:00212
410etl_ordersqueued13709702026-02-11 10:00:00NULL

Expected output

First 5 of 5 rows
run_yearfailed_runs
20226
20236
20246
20255
202623

Worked solution and explanation

What this problem is really testing

This is a filtered count over a dirty status column dressed up as a reliability review. The yearly GROUP BY is the part everyone gets right. The real exam is hidden in the data: status stores the same outcome as 'failed' and 'FAILED', and some runs never recorded a dur_secs at all. The candidates who separate themselves notice that before writing a single clause. Treat status as clean and every year comes back short; count the untimed runs and the latest year swells with failures that never recorded a runtime.

The trap, made concrete

Look at the sample rows: pipe 131 is 'failed' but pipe 286 is 'FAILED'. A filter of status = 'failed' quietly drops 286 and your yearly counts come back too low, with no error to warn you. That silent undercount is exactly the kind of bug that ships to a reliability dashboard and gets trusted for months. Normalizing the case is not optional polish here, it is correctness.

Step 1: Filter on a normalized status

Normalize the status before comparing. LOWER(status) = 'failed' catches both 'failed' and 'FAILED' in one predicate, so no uppercase failure slips through.

Step 2: Drop runs that never executed

Add dur_secs IS NOT NULL. A run that never recorded a runtime did not execute a measurable job, so it is not a genuine execution failure. On this data every one of those untimed failures lands in the latest year, so leaving them in inflates exactly the year a review reads first.

Step 3: Bucket by year and count

Derive the year with strftime('%Y', start_at) and group on that expression. Each surviving failure rolls up into its year, and COUNT(*) tallies the rows per year.

Step 4: Order chronologically

Order by run_year ascending so the timeline reads from the earliest year to the latest, which is how a reliability review scans it top to bottom.


The solution

Annual failed runs across all pipelines
SELECT
    strftime('%Y', start_at) AS run_year,
    COUNT(*) AS failed_runs
FROM data_pipes
WHERE LOWER(status) = 'failed'
  AND dur_secs IS NOT NULL
GROUP BY run_year
ORDER BY run_year ASC

Case-normalized filter plus a runtime guard, then a yearly count ordered oldest first.

Naive: `status = 'failed'`
Matches only the lowercase rows. Pipe 286 ('FAILED') is dropped, so every year's count is silently too low and no error fires.
Correct: `LOWER(status) = 'failed'`
Folds both casings into one bucket before counting, so the yearly totals reflect every real failure.

Common follow-up questions