Peak Non-Converting Month
An easy SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- easy
- Seniority
- L4
- Asked in
- ESPN
Problem
A retention review wants the single month when the most people opened the app from a recognized device, browsed, and still bought nothing from us at all. Give us that month and its count of unique visitors, and when a few months run neck and neck, the earliest is the one worth digging into first.
Everyone showed up. Nobody bought anything.
Tables
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 |
transactions
Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT
| transaction_id | user_id | product_id | quantity | total_amount | transaction_date |
|---|---|---|---|---|---|
| 1067 | 197 | 1001 | 2 | 23.46 | 2026-02-02 |
| 1134 | 294 | 1050 | 3 | 36.93 | 2026-03-03 |
| 1201 | 391 | 1099 | 4 | 50.4 | 2026-04-04 |
| 1268 | 488 | 1148 | 5 | 63.87 | 2026-05-05 |
| 1335 | 585 | 1197 | 0 | 0 | 2026-06-06 |
| 1402 | 682 | 1246 | 2 | 90.81 | 2026-07-07 |
| 1469 | 779 | 1295 | 3 | 104.28 | 2026-08-08 |
| 1536 | 876 | 1344 | 4 | 117.75 | 2026-09-09 |
| 1603 | 973 | 1393 | 5 | -45 | 2026-01-10 |
| 1670 | 1070 | 1442 | 1 | 144.69 | 2026-02-11 |
Expected output
| session_month | non_converting_users |
|---|---|
| 2026-01 | 6 |
Worked solution and explanation
What this problem is really asking
This is a global anti-join hiding inside a seasonal-analytics question. The real skill: express 'never appears in transactions' as a set difference over the WHOLE table, bucket sessions by calendar month, and count the distinct people in each. Three traps decide it. Scope the purchase check to the session's own month and you brand a shopper who bought in a different month as non-converting. Reach for COUNT(*) over COUNT(DISTINCT us.user_id) and one visitor with several sessions counts many times. Leave the tie unbroken and the engine hands back whichever of the four top months it feels like. Here COUNT(*) even crowns '2024-12' (eight sessions from five repeat visitors) over the honest winner '2026-01'.
Working through it
Step 1: Keep only visitors who never bought, table-wide
us.user_id NOT IN (SELECT user_id FROM transactions) keeps sessions whose visitor appears nowhere in transactions. The subquery is unscoped by date on purpose: any purchase we have ever recorded makes someone a buyer, so a month-matched slice would wrongly spare cross-month buyers.
Step 2: Drop sessions with no recognized device
us.device_id IS NOT NULL removes sessions that carry no recognized device before anything is counted. Skip it and those sessions inflate the buckets and shift the peak to '2025-01'.
Step 3: Bucket by calendar month and count distinct visitors
strftime('%Y-%m', us.session_start) groups each session into its year-and-month bucket as session_month. COUNT(DISTINCT us.user_id) then counts people, not rows: a visitor with three sessions in one month is one non_converting_users, not three.
Step 4: Surface the single peak, deterministically
ORDER BY non_converting_users DESC, session_month ASC with LIMIT 1 returns the busiest bucket. Four months tie at the top on this data, so the session_month ASC tiebreak is what makes '2026-01' the deterministic winner rather than whichever tied row the engine surfaces first.
The solution
SELECT strftime('%Y-%m', us.session_start) AS session_month, COUNT(DISTINCT us.user_id) AS non_converting_users FROM user_sessions us WHERE us.user_id NOT IN (SELECT user_id FROM transactions) AND us.device_id IS NOT NULL GROUP BY session_month ORDER BY non_converting_users DESC, session_month ASC LIMIT 1Common follow-up questions
- Why is `NOT EXISTS` usually preferable to `NOT IN` at this scale? (Tests planner intuition: `NOT EXISTS` correlates per outer row and streams, while `NOT IN` must materialize the whole subquery result first.)
- How would you report the peak calendar month pooled across every year instead of a single year-month? (Switches the bucket to `strftime('%m', session_start)`, pooling all Januaries, and changes whether `COUNT(DISTINCT ...)` still differs from `COUNT(*)`.)
- What breaks if `transactions.user_id` can be `NULL`? (A single `NULL` in the subquery makes `NOT IN` return no rows through three-valued logic, so the buyer set needs an `IS NOT NULL` guard or a switch to `NOT EXISTS`.)