The Long Way Back

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

Domain
SQL
Difficulty
hard
Seniority
L5
Asked in
Visa

Problem

From our service health checks we track each service's average uptime for each calendar month, leaving out any check that recorded no uptime figure. Reading one service's months in chronological order, a slump is a run of back-to-back months whose average uptime sits below the month right before it, a recovery is a run of back-to-back months whose average sits above the month right before it, a single such month already counts as a run on its own, and a month whose average ties the one right before belongs to neither and closes whichever run was open. Pair every slump with each recovery that begins after the slump's last month, and for each pairing report the service, the month the slump began, the month the recovery began, and how far the climb back went relative to the fall: the recovery's number of months divided by the slump's number of months, as a decimal.

Some services fell apart and clawed their way back. Find them, and measure the climb.

Tables

svc_health

Columns: check_id INTEGER, svc_name TEXT, status TEXT, latency REAL, uptime REAL, checked TEXT, region TEXT

svc_health: 10 of 200 rows
check_idsvc_namestatuslatencyuptimecheckedregion
137payment-apidegraded3.697.032026-02-02 01:05:00us-west-2
174user-svcunhealthy6.794.062026-03-03 02:10:00eu-west-1
211search-apitimeout9.891.092026-04-04 03:15:00ap-south-1
248gatewayHealthy12.998.122026-05-05 04:20:00eu-central-1
285notif-svcDEGRADED1695.152026-06-06 05:25:00us-east-1
322ml-servinghealthy19.192.182026-07-07 06:30:00us-west-2
359cache-svcdegradedNULL99.212026-08-08 07:35:00eu-west-1
396queue-workerunhealthy25.396.242026-09-09 08:40:00ap-south-1
433cron-svctimeout28.493.272026-01-10 09:45:00eu-central-1
470auth-svcHealthy31.590.32026-02-11 10:50:00us-east-1

Expected output

First 5 of 80 rows
svc_namedecline_startgrowth_startgrowth_ratio
auth-svc2026-032026-041
auth-svc2026-032026-062
auth-svc2026-032026-091
auth-svc2026-052026-062
auth-svc2026-052026-091

Worked solution and explanation

What this problem really is

Under the recovery story this is a gaps-and-islands problem in a comparison costume. The trap is the phrase 'a month of declining uptime': it sounds like one month-over-month drop, so candidates flag every month below its predecessor, every month above, and pair the sets. Wrong unit. A slump is a consecutive RUN of falling months collapsed into one stretch, and its length is the count of months across that run, not any single step. Treat each dip as its own event and a one-month wobble becomes a phantom slump. Quieter second trap: filter with WHERE uptime IS NOT NULL first. A month whose checks all lack an uptime reading has no real average, and left in, that empty month reads as a gap that breaks a run.


Building it in stages

Step 1: Reduce to a monthly average

Collapse the raw checks into one row per service per month holding AVG(uptime), filtering WHERE uptime IS NOT NULL first. Everything downstream compares these averages, so an empty month would enter the series and break an intact run.

Step 2: Label each month against the one before

Carry the previous month's average onto each row with LAG over month order and label it 'decline', 'growth', or 'flat' with a CASE. On its own this is only raw material: per-month verdicts, not stretches.

Step 3: Collapse consecutive months into stretches

Apply the numbering-difference trick to turn each run of same-trend months into one group, then aggregate: COUNT(*) for length and MIN(month)/MAX(month) for boundaries. Skipping this is what multiplies the row count.

Step 4: Pair slumps with later recoveries

Match every slump to every recovery that begins after it ends, via an inequality self-join on svc_name with growth_start > end_month. One slump can yield several rows, one per later recovery.

The solution

Detecting decline-then-recovery stretches
WITH monthly AS (
    SELECT svc_name, strftime('%Y-%m', checked) AS month, AVG(uptime) AS avg_uptime
    FROM svc_health
    WHERE uptime IS NOT NULL
    GROUP BY svc_name, strftime('%Y-%m', checked)
),
with_lag AS (
    SELECT svc_name, month, avg_uptime,
        LAG(avg_uptime) OVER (PARTITION BY svc_name ORDER BY month) AS prev_uptime
    FROM monthly
),
trends AS (
    SELECT svc_name, month, avg_uptime,
        CASE
            WHEN avg_uptime < prev_uptime THEN 'decline'
            WHEN avg_uptime > prev_uptime THEN 'growth'
            ELSE 'flat'
        END AS trend
    FROM with_lag
),
numbered AS (
    SELECT svc_name, month, trend,
        ROW_NUMBER() OVER (PARTITION BY svc_name ORDER BY month) AS rn
    FROM trends
),
groups AS (
    SELECT svc_name, month, trend,
        rn - ROW_NUMBER() OVER (PARTITION BY svc_name, trend ORDER BY month) AS grp
    FROM numbered
),
streaks AS (
    SELECT svc_name, trend, grp,
        MIN(month) AS start_month, MAX(month) AS end_month, COUNT(*) AS streak_len
    FROM groups
    GROUP BY svc_name, trend, grp
),
decline_then_recovery AS (
    SELECT d.svc_name, d.start_month AS decline_start, g.start_month AS growth_start,
        CAST(g.streak_len AS REAL) / d.streak_len AS growth_ratio
    FROM streaks d
    INNER JOIN streaks g
        ON d.svc_name = g.svc_name
        AND d.trend = 'decline' AND g.trend = 'growth'
        AND g.start_month > d.end_month
)
SELECT svc_name, decline_start, growth_start, growth_ratio
FROM decline_then_recovery
Per-month flags (naive)
Flag every month below its predecessor and above it, then pair the sets. Counts each wobble as its own slump, reuses months across pairs, reads lengths from single steps, and keeps empty AVG(uptime) months that split runs.
Collapsed stretches (correct)
Collapse consecutive falls and rises into stretches first, then pair them. One slump per run, COUNT(*) for true length, only real readings averaged, CAST keeping the ratio a decimal.

Common follow-up questions