Yesterday's Weather

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
L4
Asked in
Uber

Problem

A FinOps team benchmarks its cost models against the simplest possible forecast: whatever a month actually cost becomes next month's prediction. Each row in cloud_costs is a single cloud charge, and credits post as a negative amount, so total each month's real spend from the positive charges only and treat the prior month's total as the current month's forecast. For every month that has a prior month to compare against, report its year-month, its actual cost, the forecast, the absolute percent error of the forecast against that month's actual cost, and whether the actual came in over or under the forecast.

The forecast was off. By how much?

Tables

cloud_costs

Columns: cost_id INTEGER, provider TEXT, svc_name TEXT, region TEXT, amount REAL, acct_id TEXT, bill_date TEXT

cloud_costs: 10 of 200 rows
cost_idprovidersvc_nameregionamountacct_idbill_date
543GCPS3us-west-219.33acct-10012026-02-01
586AzureRDSeu-west-137.16acct-10022026-03-01
629awsLambdaap-southeast-154.99acct-10032026-04-01
672gcpBigQueryus-central172.82acct-10042026-05-01
715AWSCloud Runeurope-west190.65acct-10052026-06-01
758GCPBlob Storageus-east-1108.48acct-10062026-07-01
801AzureCosmos DBus-west-2126.31acct-10072026-08-01
844awsCloudFronteu-west-1144.14acct-10082026-09-01
887gcpPub/Subap-southeast-1161.97NULL2026-01-01
930AWSEC2us-central1179.8acct-10102026-02-01

Expected output

First 5 of 55 rows
ymactual_costforecasted_costpct_errorforecast_outcome
2022-021999.961108.4644.57589151783036over_forecast
2022-03910.831999.96119.57555196908316under_forecast
2022-04286.78910.83217.60583025315577under_forecast
2022-05732.53286.7860.850750139926014over_forecast
2022-061286.76732.5343.071746090957134over_forecast

Worked solution and explanation

What this really is

Strip the FinOps costume and this is a persistence forecast, the 'yesterday's weather' trick: each month is graded against the one immediately before it. Rolling row-level charges up to monthly totals and reaching back one month with an offset is the easy part, and anyone can write it. Candidates bleed points in two places. First the denominator: the error is measured against the actual cost, not the forecast, so the divisor is total_amount, not prev_amount. Divide by the wrong one and every number looks plausible and is wrong. Second the inputs: credits post as a negative amount, so if you sum them in you understate whatever month they land in, and that one bad month drags its neighbor's percent error into nonsense.


Break down the requirements

Step 1: Bucket bill_date by year-month and SUM the positive charges

strftime('%Y-%m', bill_date) gives the YYYY-MM key, and SUM(amount) WHERE amount > 0 keeps credits out of the actual cost. Filter in WHERE, not HAVING, because amount is a row-level column, not an aggregate. Since amount > 0 already rejects both credits and any unpriced row, no separate null test on amount is needed here.

Step 2: LAG to pull the prior month as the naive forecast

LAG(total_amount) OVER (ORDER BY ym) is prev_amount, the forecast. Then WHERE prev_amount IS NOT NULL drops the earliest month, which has no prior total to forecast from and would otherwise emit a row with a null forecast.

Step 3: Score the miss, then label its direction

pct_error is ABS((prev_amount - total_amount) / total_amount) * 100: the actual cost is the denominator and ABS makes an over-forecast and an under-forecast of the same size count equally. A CASE then compares total_amount to prev_amount to tag each month over_forecast or under_forecast, so the magnitude and the direction of the miss sit side by side.


The solution

Monthly aggregate, LAG forecast, percent error and direction
WITH monthly AS (
  SELECT strftime('%Y-%m', bill_date) AS ym, SUM(amount) AS total_amount
  FROM cloud_costs
  WHERE amount > 0
  GROUP BY strftime('%Y-%m', bill_date)
),
forecast AS (
  SELECT ym, total_amount, LAG(total_amount) OVER (ORDER BY ym) AS prev_amount FROM monthly
)
SELECT
  ym,
  total_amount AS actual_cost,
  prev_amount AS forecasted_cost,
  ABS((prev_amount - total_amount) / total_amount) * 100 AS pct_error,
  CASE
    WHEN total_amount > prev_amount THEN 'over_forecast'
    WHEN total_amount < prev_amount THEN 'under_forecast'
    ELSE 'on_forecast'
  END AS forecast_outcome
FROM forecast
WHERE prev_amount IS NOT NULL

Common follow-up questions