Feature Flag Engagement Impact
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
- General Assembly
Problem
The growth team wants to understand session activity around feature flag launches. For each feature flag, return the flag name, a display name (underscores replaced with spaces), whether the flag is currently enabled, and how many user sessions started in the 30 days following the flag's created date. Flags with zero qualifying sessions should still appear with a session count of zero. Sort by flag name.
Flags on versus flags off. The engagement gap.
Tables
feat_flags
Columns: flag_id INTEGER, flag_name TEXT, enabled INTEGER, rollout REAL, owner TEXT, created TEXT, updated TEXT
| flag_id | flag_name | enabled | rollout | owner | created | updated |
|---|---|---|---|---|---|---|
| 131 | new_checkout | 0 | NULL | engineering | 2025-02-02 | 2026-02-02 |
| 162 | beta_search | 0 | NULL | growth | 2025-03-03 | 2026-03-03 |
| 193 | ai_recs | 1 | 39 | platform | 2025-04-04 | 2026-04-04 |
| 224 | live_chat | 0 | NULL | mobile | 2025-05-05 | 2026-05-05 |
| 255 | video_upload | 0 | NULL | data | 2025-06-06 | 2026-06-06 |
| 286 | two_factor | 1 | 78 | product | 2025-07-07 | 2026-07-07 |
| 317 | bulk_export | 0 | NULL | engineering | 2025-08-08 | 2026-08-08 |
| 348 | new_dashboard | 0 | NULL | growth | 2025-09-09 | 2026-09-09 |
| 379 | mobile_v2 | 1 | 17 | platform | 2025-10-10 | 2026-01-10 |
| 410 | dark_mode | 0 | NULL | mobile | 2025-11-11 | 2026-02-11 |
user_sessions
Columns: session_id INTEGER, user_id INTEGER, device_id INTEGER, session_start TEXT, session_duration_sec INTEGER, pages_viewed INTEGER
| session_id | user_id | device_id | session_start | session_duration_sec | pages_viewed |
|---|---|---|---|---|---|
| 7061 | 197 | 230 | 2026-02-02 01:00:00 | 53 | 3 |
| 7122 | 294 | 259 | 2026-03-03 02:00:00 | 76 | 6 |
| 7183 | 391 | 288 | 2026-04-04 03:00:00 | 99 | 9 |
| 7244 | 488 | 317 | 2026-05-05 04:00:00 | 122 | 12 |
| 7305 | 585 | 346 | 2026-06-06 05:00:00 | 145 | 15 |
| 7366 | 682 | 375 | 2026-07-07 06:00:00 | 168 | 18 |
| 7427 | 779 | 404 | 2026-08-08 07:00:00 | 191 | 21 |
| 7488 | 876 | NULL | 2026-09-09 08:00:00 | 214 | 24 |
| 7549 | 973 | 462 | 2026-01-10 09:00:00 | 237 | 27 |
| 7610 | 1070 | 491 | 2026-02-11 10:00:00 | 260 | 30 |
push_notifs
Columns: notif_id INTEGER, user_id INTEGER, title TEXT, platform TEXT, status TEXT, sent_at TEXT, opened INTEGER, campaign TEXT
| notif_id | user_id | title | platform | status | sent_at | opened | campaign |
|---|---|---|---|---|---|---|---|
| 1053 | 100 | New message from team | android | pending | 2026-02-02 01:07:00 | 0 | winback_feb |
| 1106 | 197 | Weekly digest ready | web | failed | 2026-03-03 02:14:00 | 0 | promo_summer |
| 1159 | 294 | Price drop alert | iOS | bounced | 2026-04-04 03:21:00 | 1 | onboard_v2 |
| 1212 | 391 | Security alert | Android | Delivered | 2026-05-05 04:28:00 | 0 | NULL |
| 1265 | 488 | Payment received | WEB | FAILED | 2026-06-06 05:35:00 | NULL | loyalty_2024 |
| 1318 | 585 | Complete your profile | basic | delivered | 2026-07-07 06:42:00 | 1 | NULL |
| 1371 | 682 | New feature available | premium | pending | 2026-08-08 07:49:00 | 0 | reengagement |
| 1424 | 779 | Subscription renewal | ios | failed | 2026-09-09 08:56:00 | 0 | NULL |
| 1477 | 876 | Flash sale: 50% off | android | bounced | 2026-01-10 09:03:00 | 1 | flash_sale |
| 1530 | 973 | Your order shipped! | web | Delivered | 2026-02-11 10:10:00 | NULL | retention_q1 |
Expected output
| flag_name | display_name | enabled | launch_window_sessions |
|---|---|---|---|
| ai_recs | ai recs | 1 | 0 |
| beta_search | beta search | 0 | 7 |
| bulk_export | bulk export | 0 | 0 |
| dark_mode | dark mode | 0 | 5 |
| live_chat | live chat | 0 | 0 |
Worked solution and explanation
The mental model
Experimentation teams use this to check one habit: do you treat a date-bounded inequality as a JOIN condition or a WHERE filter? Both compute different answers, and only one preserves flags that had zero qualifying sessions. The interviewer is watching whether you reach for LEFT JOIN ... ON DATE before they have to ask, and whether you say COUNT(us.session_id) instead of COUNT(*) without prompting.
The two traps that will burn you
Step 1: Trap 1: WHERE kills your LEFT JOIN
If you write the window filter in WHERE (WHERE us.session_start >= ff.created AND us.session_start < ...), the LEFT JOIN silently becomes an INNER JOIN. SQL evaluates ON first, producing NULL rows for unmatched flags, then WHERE filters those NULLs out (because NULL fails any comparison). Result: flags with zero sessions vanish from your output and you fail the 'still appear with zero' requirement. Fix: put the window predicate inside ON.
Step 2: Trap 2: COUNT(*) lies on a LEFT JOIN
On a LEFT JOIN, unmatched flags produce one synthetic row with NULL in every right-side column. COUNT(*) counts that synthetic row as 1, so a flag with no sessions reports launch_window_sessions = 1. COUNT(us.session_id) skips the NULL and correctly reports 0. Always count a specific right-side column when LEFT JOIN is involved.
The solution
SELECT
ff.flag_name,
REPLACE(ff.flag_name, '_', ' ') AS display_name,
ff.enabled,
COUNT(us.session_id) AS launch_window_sessions
FROM feat_flags ff
LEFT JOIN user_sessions us
ON DATE(us.session_start) >= DATE(ff.created)
AND DATE(us.session_start) < DATE(ff.created, '+30 days')
GROUP BY ff.flag_id, ff.flag_name, ff.enabled
ORDER BY ff.flag_nameCommon follow-up questions
- Walk me through what happens if I move the date predicate from ON into WHERE. Will the output change? (The interviewer is testing whether the candidate knows that ON-clause filters preserve LEFT JOIN semantics while WHERE filters collapse them. A common follow-up after the candidate submits a wrong query.)
- If I swap COUNT(`us.session_id`) for COUNT(*), what value does a flag with zero matching sessions now report, and why? (Tests COUNT semantics on outer joins. The candidate should explain that COUNT(*) counts synthetic NULL rows generated by LEFT JOIN.)
- The product team now wants 'rolling 7-day' instead of '30-day fixed'. What changes? (Tests time-bucketing literacy. Real answer: yes, recompute the windows from creation, do not assume midnight-aligned days.)
- The query takes 4 minutes in production. The sessions table is partitioned by day. What is the first thing you would change? (Tests partition awareness. `user_sessions` is partitioned on `session_start`; the planner can prune partitions if you push down a global date range, but per-flag windows defeat that. The fix is generating the date range as min(created) to max(created)+30 globally.)