Below the Peaks
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
Problem
Finance is auditing outlier allocations within each team. Surface the allocations that land above their team's own average yet outside that team's five biggest by amount, and return the team name and the amount.
The biggest bills catch every eye. The overspend hiding just beneath them does not.
Tables
cost_allocs
Columns: alloc_id INTEGER, team_name TEXT, svc_name TEXT, region TEXT, amount REAL, period TEXT, acct_id TEXT, category TEXT
| alloc_id | team_name | svc_name | region | amount | period | acct_id | category |
|---|---|---|---|---|---|---|---|
| 129 | data-eng | S3 | us-west-2 | 83.21 | 2026-02 | acct-101 | storage |
| 158 | ml-team | RDS | eu-west-1 | 156.42 | 2026-03 | acct-102 | network |
| 187 | frontend | Lambda | ap-south-1 | 229.63 | 2026-04 | acct-103 | database |
| 216 | backend | BigQuery | us-central1 | -120.5 | 2026-05 | acct-104 | ml |
| 245 | devops | CloudFront | europe-west1 | 376.05 | 2026-06 | acct-105 | other |
| 274 | security | EKS | ap-southeast-1 | 449.26 | 2026-07 | acct-106 | compute |
| 303 | mobile | SQS | eu-central-1 | 522.47 | 2026-08 | acct-107 | storage |
| 332 | Platform | EC2 | us-east-1 | 595.68 | 2026-09 | NULL | network |
| 361 | DATA-ENG | S3 | us-west-2 | 668.89 | 2026-01 | acct-109 | database |
| 390 | platform | RDS | eu-west-1 | 742.1 | 2026-02 | acct-110 | ml |
Expected output
| team_name | amount |
|---|---|
| DATA-ENG | 9857.04 |
| DATA-ENG | 10768.34 |
| DATA-ENG | 11679.64 |
| DATA-ENG | 12590.94 |
| DATA-ENG | 13502.24 |
Worked solution and explanation
What this really is
Strip the finance costume off and this is a per-team double filter: keep the rows a team spent above its own average, then discard that team's five biggest. The two conditions look like siblings, but they are computed over different populations, and that is exactly where candidates fall in. Anyone can write an AVG and a ROW_NUMBER. The tell is whether your average is taken over the WHOLE team before any rows are dropped, and whether your rank threshold lands on rn > 5 rather than rn >= 5.
The trap: what the average is averaged over
Step 1: Average the whole team, exactly once
Build a team_avg CTE that groups by team_name and takes AVG(amount) across every allocation the team has. Keeping this in its own scope guarantees the mean is computed over the full population, so no later filter can distort it. This is the number every candidate row will be measured against.
Step 2: Number each team's rows from the top
In a second CTE, assign ROW_NUMBER() OVER (PARTITION BY team_name ORDER BY amount DESC). Partitioning restarts the count for each team, and ordering by amount descending puts the biggest allocation at rn = 1. Now 'outside the top five' is just a predicate on rn.
Step 3: Join, then apply both filters
Join the numbered rows back to team_avg on team_name so every row carries its own team's mean, then keep rows where amount is above that mean AND rn is greater than 5. Both conditions are independent predicates on the same row, ANDed together: pass both to survive.
WITH team_avg AS (
SELECT team_name, AVG(amount) AS avg_amount
FROM cost_allocs
GROUP BY team_name
),
ranked AS (
SELECT ca.team_name, ca.amount,
ROW_NUMBER() OVER (PARTITION BY ca.team_name ORDER BY ca.amount DESC) AS rn
FROM cost_allocs ca
)
SELECT r.team_name, r.amount
FROM ranked r
INNER JOIN team_avg ta ON r.team_name = ta.team_name
WHERE r.amount > ta.avg_amount AND r.rn > 5Two CTEs, one join: the mean is computed over the full team, the rank threshold excludes the top five.
- GROUP BY plus join (canonical)
- Aggregate team averages in one CTE, number rows in another, then join on
team_nameand filter. Clear, explicit, and easy to reason about; costs two scans and a hash join. - Two window functions, no join
- SELECT
team_name, amount FROM (SELECTteam_name, amount, AVG(amount) OVER (PARTITION BYteam_name) ASteam_avg,ROW_NUMBER() OVER (PARTITION BYteam_nameORDER BY amount DESC) AS rn FROMcost_allocs) t WHERE amount >team_avgAND rn > 5. One scan, both per-team values computed inline, no join to maintain.
Common follow-up questions
- If two allocations tie on amount right at the fifth position, which one is 'outside the top five'? (Probes `ROW_NUMBER` versus RANK/`DENSE_RANK` and whether ties should share a position or be broken arbitrarily.)
- Team names arrive with inconsistent casing, like data-eng and DATA-ENG. Should those collapse into one team? (Tests whether the candidate spots that grouping on the raw column keeps them separate and when to normalize with LOWER(`team_name`).)
- How would you tighten the rule to only flag allocations at least 20 percent above the team average? (Parameterizing the threshold: amount > `avg_amount` * 1.2 rather than a bare comparison.)