Peak Concurrent Batch Jobs

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

Domain
SQL
Difficulty
medium
Seniority
L4

Problem

A job holds its slot from its start up to its end, so one that ends at the exact instant another starts never overlaps it, and a job with no recorded end has never given its slot back. Across batch_jobs, what is the most jobs we ever had holding a slot at the same instant?

Jobs pile up. Find the moment the scheduler sweats the most.

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
peak
51

Worked solution and explanation

The disguise

This is interval overlap wearing a capacity-planning costume. Underneath it: given a pile of [start, end) windows, some of which never closed, what is the most that overlap at any single instant? Anyone can filter the table. What separates candidates is the two edges of an interval. An ending job leaves before a job starting at that same second arrives, and a job with no end never leaves at all. Get the tie backwards and every touching pair inflates the peak. Emit a release for an unfinished job and its NULL timestamp sorts ahead of every real event, so every running count is dragged down.

Correct but quadratic

The self-join instinct
SELECT MAX(concurrent) AS peak
FROM (
    SELECT a.job_id, COUNT(*) AS concurrent
    FROM batch_jobs a
    JOIN batch_jobs b
      ON b.started <= a.started
     AND (b.ended IS NULL OR b.ended > a.started)
    GROUP BY a.job_id
) t;

Right answer, wrong cost: every job is compared against every other job.

The peak always occurs at some job's start, so it is enough to count, at each start, the jobs live at that instant: started no later, and either still open or ending after it. The problem is the pairing. It inspects every (a, b) combination, which is O(n^2). On 50k rows that is 2.5 billion comparisons. A good candidate writes this, then says out loud that it will not scale.

The line sweep

Step 1: Claim for every job, release only for finished ones

Every batch_jobs row claims a slot, so every row emits started with a +1. Only a finished job releases one, so the release half emits ended with a -1 and keeps just the rows where ended IS NOT NULL. A UNION ALL glues the halves without deduplicating, which matters: two jobs starting in the same second are two events.

Step 2: Order the events, ends before starts on a tie

Sort every event by timestamp. When one job ends at the same instant another starts, the ending job leaves first, so the two never share a slot. Sorting by ts, delta puts -1 before +1 at an identical timestamp, which encodes the half-open [started, ended) for free. Flip that tiebreak and you count inclusive-inclusive.

Step 3: Take the max of the running sum

Walk the sorted events keeping a running total: +1 raises the count of live jobs, -1 lowers it. SUM(delta) OVER (ORDER BY ts, delta ROWS UNBOUNDED PRECEDING) is that prefix sum, the number of jobs alive right after each event, and MAX() reads off the worst moment.

Line sweep
WITH events AS (
    SELECT started AS ts, 1 AS delta FROM batch_jobs
    UNION ALL
    SELECT ended AS ts, -1 AS delta FROM batch_jobs WHERE ended IS NOT NULL
)
SELECT MAX(running) AS peak FROM (
    SELECT SUM(delta) OVER (ORDER BY ts, delta ROWS UNBOUNDED PRECEDING) AS running
    FROM events
)

One pass to build events, one ordered scan for the running total, one MAX to read the peak.

Inclusive-inclusive [start, end]
A job ending at 10:00 and one starting at 10:00 both count at 10:00. That means sorting +1 before -1 on ties, and the peak comes out inflated by one at every shared boundary.
Half-open [start, end)
The ending job releases its slot before the newcomer claims one, so sort -1 before +1 on ties. This matches how real capacity frees up and is the convention the prompt asks for.

Common follow-up questions