Satisfaction by Platform
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
- L3
- Asked in
- Walmart
Problem
Give us the average experiment outcome score for the 25-34 age cohort on each platform, with platforms told apart by their labels exactly as our event logs captured them, a missing label included. Our scoring rubric credits only the whole points of each logged outcome, so drop each score's fraction before averaging, and report the average rounded to the nearest whole number.
Satisfaction scores, platform by platform.
Tables
experiments
Columns: exp_id INTEGER, exp_name TEXT, variant TEXT, user_id INTEGER, outcome REAL, created TEXT, platform TEXT
| exp_id | exp_name | variant | user_id | outcome | created | platform |
|---|---|---|---|---|---|---|
| 1053 | checkout_v3 | variant_a | 100 | 11.3 | 2026-02-02 | android |
| 1106 | onboard_flow | variant_b | 197 | 22.6 | 2026-03-03 | web |
| 1159 | pricing_tier | variant_c | 294 | 33.9 | 2026-04-04 | iOS |
| 1212 | search_rank | holdout | 391 | 45.2 | 2026-05-05 | Android |
| 1265 | rec_model_b | control | 488 | 56.5 | 2026-06-06 | NULL |
| 1318 | notif_freq | variant_a | 585 | NULL | 2026-07-07 | WEB |
| 1371 | home_layout | variant_b | 682 | 79.1 | 2026-08-08 | NULL |
| 1424 | new_feed_algo | variant_c | 779 | 90.4 | 2026-09-09 | ios |
| 1477 | checkout_v3 | holdout | 876 | 1.7 | 2026-01-10 | android |
| 1530 | onboard_flow | control | 973 | 13 | 2026-02-11 | web |
users
Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT
| user_id | username | signup_date | account_status | age_bucket | |
|---|---|---|---|---|---|
| 100 | alice | alice@example.com | 2025-02-02 | inactive | 25-34 |
| 197 | aaron42 | aaron42@example.com | 2026-03-03 | suspended | 35-44 |
| 294 | amelia | amelia@example.com | 2024-04-04 | pending_verification | 45-54 |
| 391 | arjun | arjun@example.com | 2025-05-05 | active | 55-64 |
| 488 | ava99 | ava99@example.com | 2026-06-06 | inactive | 65+ |
| 585 | andrew_k | andrew_k@example.com | 2024-07-07 | suspended | NULL |
| 682 | anika | anika@example.com | 2025-08-08 16:34:00 | pending_verification | 18-24 |
| 779 | aiden | aiden@example.com | 2026-09-09 14:53:00 | active | 25-34 |
| 876 | aria_b | aria_b@example.com | 2024-10-10 | inactive | 35-44 |
| 973 | brian | brian@example.com | 2025-11-11 | suspended | 45-54 |
Expected output
| platform | avg_outcome_score |
|---|---|
| NULL | 40 |
| Android | 39 |
| WEB | 48 |
| android | 28 |
| iOS | 52 |
Worked solution and explanation
What this really is
Strip the growth framing and this is a filtered group-average across two tables with one conversion hidden in the order of operations. The score and the label live in experiments, the cohort lives in users.age_bucket, so the join exists only to carry the cohort onto the rows you average. Everyone gets the join. What separates candidates is where the whole-point conversion goes: it has to hit each score before AVG, not the average after it. Put it last and 'Android' reports 40 and 'WEB' reports 49, where the rubric says 39 and 48.
Build it in four moves
Step 1: Bring age_bucket next to the score
Join experiments to users on user_id. An inner join is right: an event only matters if its user can be placed in an age bucket, and users with no matching row have no cohort to belong to.
Step 2: Keep only the 25-34 cohort
Filter u.age_bucket = '25-34' in the WHERE. It runs before the grouping, so it shrinks the set you aggregate, which is also the cheaper plan.
Step 3: Drop each score's fraction before averaging
CAST(e.outcome AS INTEGER) truncates toward zero, so 48.6 becomes 48. A NULL outcome stays NULL through the cast, and AVG skips it, so missing scores never dilute a platform. Every score on this data is positive, which means FLOOR would land on the same values.
Step 4: Average per raw label, then round
Group on e.platform exactly as stored and take ROUND(AVG(...)) over the whole points. For 'android' the scores 11 and 44 average to 27.5, which rounds to 28.
SELECT e.platform, ROUND(AVG(CAST(e.outcome AS INTEGER))) AS avg_outcome_score
FROM experiments e
INNER JOIN users u ON e.user_id = u.user_id
WHERE u.age_bucket = '25-34'
GROUP BY e.platformThe cast sits inside the average and the round sits outside it.
- Round the raw average
ROUND(AVG(e.outcome))averages 'Android' to 39.6 and rounds up to 40, and averages 'WEB' to 48.6 and rounds up to 49. The fraction the rubric never awarded pushes both over the line.- Average the whole points
ROUND(AVG(CAST(e.outcome AS INTEGER)))averages 39 and 48 for those two labels. The 0.6 is gone before the mean is taken, so it cannot round anything up.
Common follow-up questions
- Some events have a `NULL` outcome. How would the averages change if the business wanted those counted as zero points? (Tests whether the candidate knows `AVG` skips NULLs and how a default changes the denominator.)
- If product later decides 'android' and 'Android' are the same platform, where would you normalize, and what breaks if you do it in the wrong place? (Probes case-normalization tradeoffs without breaking the output contract.)
- What happens to the whole-point rule if a score can be negative, and would `FLOOR` still match the cast? (Tests whether the candidate knows the cast truncates toward zero while `FLOOR` rounds down.)