Power Users
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
Problem
We're shortlisting repeat users for a case-study writeup, and anyone with 3 or more sessions in user_sessions makes the cut. List each one's username from users with their session count, most sessions first.
Engagement separates tourists from regulars.
Tables
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 |
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 |
Expected output
| username | session_count |
|---|---|
| alice | 8 |
| aaron42 | 5 |
| amelia | 5 |
| jasper | 3 |
| jin | 3 |
Worked solution and explanation
This is a filter on a number that does not exist yet, dressed up as a shortlist. Each user's session count only appears once user_sessions is collapsed per account, so the 3-or-more cut has to run after that collapse, in HAVING, never in WHERE. Anyone can write the join and the count. What separates candidates is where the threshold lives and what the group key is. Put COUNT(*) >= 3 in WHERE and the engine rejects the query. Leave the cut out and 59 users come back instead of 48, the eleven one- and two-session users included.
Walking it
Step 1: Join users to user_sessions
An inner join on user_id gives one row per session, tagged with its account. Inner is enough: a user with no sessions can never reach three. A LEFT JOIN only adds rows the cut throws away, and COUNT(*) would score each of them as 1, not 0.
Step 2: Group on the account, not the name
Use GROUP BY u.user_id, u.username. user_id is what makes a row one person, and username rides along so the SELECT can show it. Grouping on username alone would fold any two accounts that share a display name into one inflated row.
Step 3: Cut after counting
HAVING COUNT(*) >= 3 runs once every group's count is known. That is the whole problem. WHERE sees single session rows, and a single row has no count to compare.
Step 4: Sort by the count alone
Use ORDER BY session_count DESC. On this data one user has 8 sessions, 19 have 5 and 28 have 3. Users tied on a count may come back in any order, so no tiebreaker is owed.
SELECT u.username, COUNT(*) AS session_count
FROM users u
JOIN user_sessions s ON u.user_id = s.user_id
GROUP BY u.user_id, u.username
HAVING COUNT(*) >= 3
ORDER BY session_count DESC- Threshold in WHERE
WHERE COUNT(*) >= 3asks each session row about a total that has not been computed yet. The engine refuses it: aggregates are not allowed inWHERE.- Threshold in HAVING
HAVING COUNT(*) >= 3is evaluated per group afterGROUP BY. Every account's count exists by then, and the 48 qualifying users survive.
Common follow-up questions
- How would you keep the users whose session count is in the top 10% instead of a fixed 3? (Replacing a literal threshold with one derived from the distribution of per-user counts.)
- What changes if only this year's sessions should count toward the shortlist? (Whether the date predicate belongs on rows in `WHERE` while the count threshold stays in `HAVING`.)
- How would you also show each user's most recent session? (Adding a second aggregate, `MAX(session_start)`, to the same groups.)