Past the Threshold
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
Problem
Our retention analyst calls a user engaged once they have more than 3 sessions in user_sessions: which users qualify, and how many sessions does each one have?
Somewhere after the third visit, a habit quietly forms. Find the ones who crossed over.
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 |
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
| user_id | session_count |
|---|---|
| 100 | 8 |
| 197 | 5 |
| 294 | 5 |
| 391 | 5 |
| 488 | 5 |
Worked solution and explanation
This is a per-user threshold dressed up as a retention metric. The cutoff belongs to a user's whole history, not to any one session, so no row in user_sessions can tell you whether its owner qualifies: the count has to exist first, and the filter has to run on it. Everyone writes the count. What separates candidates is where the cutoff lands and what gets counted. Put it on the rows and the query errors or filters the wrong thing. Count device_id instead of sessions and four engaged users silently vanish. Read 'more than 3' as 'at least 3' and the list grows from 20 users to 48.
Getting to the answer
Step 1: Stay inside user_sessions
Every piece of the answer, user_id and the sessions themselves, lives in user_sessions. The users table is a temptation: joining it adds a second scan and changes no row of the result, because the session's user_id already identifies the user.
Step 2: Count sessions, not a column
COUNT(*) counts rows, and each row is a session. COUNT(device_id) counts non-NULL devices, and 23 sessions have no device recorded. User 876 has five sessions, three of them without a device, so the column count reports 2 and pushes them below the cutoff.
Step 3: Filter the count once it exists
The condition is about each user's total, so it goes in HAVING, which runs once per group after COUNT(*) is computed. WHERE runs per session before any grouping and cannot see the count; most engines reject WHERE COUNT(*) > 3 outright.
SELECT user_id, COUNT(*) AS session_count
FROM user_sessions
GROUP BY user_id
HAVING COUNT(*) > 3- Counting rows
COUNT(*): user876has 5 sessions and qualifies, and user100has 8. Twenty users clear the bar.- Counting a column
COUNT(device_id): NULL devices are skipped, so876drops to 2 and100to 6. Four of the twenty engaged users disappear without any error.
Common follow-up questions
- Engaged now means more than 3 sessions in the last 30 days. Where does the date condition go, and why does it differ from the session cutoff? (Tests row-level vs group-level filtering in one query: the date is a `WHERE` on `session_start`, and the count stays in `HAVING`.)
- Retention also wants each engaged user's `username` next to the count. How do you add it without counting sessions twice? (Tests aggregating before joining to `users`, so the join cannot fan out the counts.)
- What if the cutoff becomes the top 10% of users by session count instead of a fixed 3? (Tests replacing a literal threshold with one computed over every user's count.)