Ghosts in the Scheduler

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
Southwest Airlines

Problem

Status values in batch_jobs were typed by hand, so a finished job can read 'completed' in more than one casing, and some of those completed jobs never wrote an ended timestamp. For the platform team's audit of jobs started in 2024, count the jobs marked completed, in any casing, that still have no end time recorded.

It says running. It has been running.

Tables

batch_jobs

Columns: job_id INTEGER, job_name TEXT, status TEXT, rows_done INTEGER, started TEXT, ended TEXT, priority INTEGER, retries INTEGER

batch_jobs: 10 of 200 rows
job_idjob_namestatusrows_donestartedendedpriorityretries
1047user_exportrunning972026-02-02 01:00:002026-02-02 03:11:0030
1094data_cleanupfailed1942026-03-03 02:00:002026-03-03 04:22:0060
1141email_blastqueued2912026-04-04 03:00:002026-04-04 05:33:0090
1188index_rebuildcanceled3882026-05-05 04:00:00NULL20
1235cache_warmCompleted4852026-06-06 05:00:002026-06-06 07:55:0050
1282log_archiveFAILEDNULL2026-07-07 06:00:002026-07-07 08:06:0080
1329metric_aggcompleted6792026-08-08 07:00:002026-08-08 09:17:0010
1376backup_dbrunning7762026-09-09 08:00:00NULL40
1423sync_elasticfailed8732026-01-10 09:00:002026-01-10 11:39:0075
1470daily_reportqueued9702026-02-11 10:00:002026-02-11 12:50:0000

Expected output

First 1 of 1 rows
completed_without_end_count
2

Worked solution and explanation

What this really is

Strip the costume and this is a count behind three filters, and one of them is a lie detector. The count is trivial. What separates candidates is noticing that status was typed by hand, so the same outcome arrives as 'completed' and 'Completed'. A plain status = 'completed' runs clean and returns 1 for 2024 when the real answer is 2: half the ghosts vanish, and nobody is warned. The second filter is the absent end time, which only ended IS NULL sees; ended = NULL matches nothing at all.

Build it up

Step 1: Fence the calendar year on started

started is text in 'YYYY-MM-DD HH:MM:SS' form, so string comparison sorts chronologically. Use started >= '2024-01-01' AND started < '2025-01-01': the half-open bound keeps a job started at 23:00 on December 31 and leaves January 1 of the next year out.

Step 2: Fold the casing before comparing

LOWER(status) = 'completed' maps 'Completed' onto 'completed', so both spellings of the same outcome count. String equality is case-sensitive here, so skipping this silently drops a real row.

Step 3: Test for absence, not equality

ended IS NULL keeps the jobs that never wrote an end time. NULL is not equal to anything, itself included, so ended = NULL evaluates to unknown on every row and keeps none.

Step 4: Count the survivors

COUNT(*) collapses what is left into a single number, named completed_without_end_count so the result says what it measures.

Count completed jobs with no end time
SELECT COUNT(*) AS completed_without_end_count
FROM batch_jobs
WHERE started >= '2024-01-01'
  AND started < '2025-01-01'
  AND ended IS NULL
  AND LOWER(status) = 'completed'
Trusts the data
status = 'completed' returns 1, and swapping in ended = NULL drops it to 0. Both run without an error.
Distrusts the data
LOWER(status) = 'completed' with ended IS NULL returns 2, the real number of jobs that claim success but never finished.

Common follow-up questions