Service Budget per Head
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
- Asked in
- Microsoft
Problem
Team names in cost_allocs were typed by hand, so a name that differs only in letter case is still the same team. A service's budget exposure grows with its billing volume: every allocation dollar counts once for each line that service has in cloud_costs, and a service with no billing lines is left off the report entirely. An analyst wants each service's budget per head, its exposure split evenly across its unique teams, rounded to a whole number.
Budget per head. Pipeline by pipeline.
Tables
cloud_costs
Columns: cost_id INTEGER, provider TEXT, svc_name TEXT, region TEXT, amount REAL, acct_id TEXT, bill_date TEXT
| cost_id | provider | svc_name | region | amount | acct_id | bill_date |
|---|---|---|---|---|---|---|
| 543 | GCP | S3 | us-west-2 | 19.33 | acct-1001 | 2026-02-01 |
| 586 | Azure | RDS | eu-west-1 | 37.16 | acct-1002 | 2026-03-01 |
| 629 | aws | Lambda | ap-southeast-1 | 54.99 | acct-1003 | 2026-04-01 |
| 672 | gcp | BigQuery | us-central1 | 72.82 | acct-1004 | 2026-05-01 |
| 715 | AWS | Cloud Run | europe-west1 | 90.65 | acct-1005 | 2026-06-01 |
| 758 | GCP | Blob Storage | us-east-1 | 108.48 | acct-1006 | 2026-07-01 |
| 801 | Azure | Cosmos DB | us-west-2 | 126.31 | acct-1007 | 2026-08-01 |
| 844 | aws | CloudFront | eu-west-1 | 144.14 | acct-1008 | 2026-09-01 |
| 887 | gcp | Pub/Sub | ap-southeast-1 | 161.97 | NULL | 2026-01-01 |
| 930 | AWS | EC2 | us-central1 | 179.8 | acct-1010 | 2026-02-01 |
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
| svc_name | budget_per_head |
|---|---|
| BigQuery | 509340 |
| CloudFront | 521188 |
| EC2 | 390893 |
| Lambda | 505513 |
| RDS | 546230 |
Worked solution and explanation
What this really is
This is a fan-out you keep on purpose, with a divisor that has to survive it. Joining cloud_costs to cost_allocs on svc_name copies every allocation row once per billing line, and the statement wants exactly that in the numerator. Anyone can write the join. What separates candidates is seeing that the same copies land in the divisor too, and that only a unique count of teams is immune to them. The second trap is quieter: team_name was typed by hand, so one team appears in two letter cases. Count the raw spellings and every affected service splits its exposure across a phantom team, landing well under the true figure.
Build it in three moves
Step 1: Match on svc_name and nothing else
An INNER JOIN on svc_name keeps only services that have both billing lines and allocated teams, which is the report's row set. Adding region or acct_id to the condition looks careful and silently shrinks every total.
Step 2: Let SUM(ca.amount) fan out
After the join each allocation dollar appears once per billing line of its service, so the plain sum already is the exposure. Deduping cloud_costs before the join removes the very multiplier the statement asks for.
Step 3: Count each team once, whatever its casing
COUNT(DISTINCT LOWER(ca.team_name)) collapses the join copies and the case variants into one entry per real team. Multiplying by 1.0 before dividing keeps an integer amount from truncating, and ROUND takes it to a whole number.
SELECT cc.svc_name,
ROUND(SUM(ca.amount) * 1.0 / COUNT(DISTINCT LOWER(ca.team_name))) AS budget_per_head
FROM cloud_costs cc
INNER JOIN cost_allocs ca ON cc.svc_name = ca.svc_name
GROUP BY cc.svc_nameThe join shapes the numerator; LOWER inside the unique count keeps the divisor honest.
SELECT cc.svc_name,
ROUND(SUM(ca.amount) * 1.0 / COUNT(ca.team_name)) AS budget_per_head
FROM cloud_costs cc
INNER JOIN cost_allocs ca ON cc.svc_name = ca.svc_name
GROUP BY cc.svc_nameWrong: the divisor counts fanned-out rows, not teams.
- Spelling as written
COUNT(DISTINCT ca.team_name)treats 'Data Platform' and 'data platform' as two teams, so the divisor is too big wherever the casing drifted.- One team per name
COUNT(DISTINCT LOWER(ca.team_name))folds the variants together, and every service splits its exposure across the teams that actually exist.
Common follow-up questions
- If `cloud_costs` held accidental duplicate billing lines, what happens to `budget_per_head`, and how would you guard against it? (The numerator scales with billing lines by design, so duplicate lines inflate exposure silently.)
- Rewrite this so the join touches 300 rows per side instead of billions. (Pre-aggregate both sides by `svc_name`, then multiply the allocation total by the billing-line count.)
- What if `team_name` also had stray whitespace or abbreviations, and where should that cleanup live? (Folding case fixes one kind of drift; trailing spaces or abbreviations need a cleaned team dimension.)