User Session Roster
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
- Ripple
Problem
Our usernames were typed in by hand, so some carry stray spaces or capital letters, and we read each one as its trimmed, lowercase form. From users and user_sessions, build a roster of each user's cleaned name, account status, signup date and session start that keeps people who have never started a session, sorted by cleaned name and then by session start, earliest first.
Every user paired with their sessions, even users who never logged in
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 | account_status | signup_date | session_start |
|---|---|---|---|
| aaron42 | suspended | 2026-03-03 | 2026-01-26 09:00:00 |
| aaron42 | suspended | 2026-03-03 | 2026-02-02 01:00:00 |
| abel | active | 2026-04-08 | NULL |
| alice | inactive | 2023-05-25 | NULL |
| amelia | pending_verification | 2024-04-04 | 2026-02-27 10:00:00 |
Worked solution and explanation
What this roster is really asking
This is an outer join dressed up as a reporting chore, with a cleaning step riding on top. Everyone links users to user_sessions on user_id. What separates candidates is whether the people who never logged in survive. Reach for an inner join because the ask sounds like 'users and their sessions' and every session-less user disappears without an error. The headcount comes back short, and nobody notices until onboarding numbers stop adding up.
The second wrinkle is the name itself. The username column holds ' amelia' with a leading space, 'arjun ' with a trailing one, and 'Alice' next to 'alice'. Sorted raw, the padded and capitalized names land in the wrong place. The statement's rule of reading a name as its trimmed, lowercase form has to happen before the sort.
Building it
Step 1: Keep users on the preserved side
Start FROM users and LEFT JOIN user_sessions on user_id. A user with sessions fans out to one row per session. A user with none still yields exactly one row, with session_start filled in as NULL. That NULL row is the whole point of the question.
Step 2: Clean the name once, in the select list
Compute LOWER(TRIM(u.username)) AS username directly in the SELECT and sort on that alias. The cleaned value is written once, so the displayed name and the sort key can never disagree, and no intermediate step is needed for a query this flat.
Step 3: Sort by the cleaned name, then by session start
ORDER BY username, session_start walks the roster alphabetically by cleaned name and, within a name, from the earliest session onward. Because username resolves to the alias, 'Alice' and 'alice' both become 'alice' and their rows sort together by session_start, instead of splitting across the list.
SELECT
LOWER(TRIM(u.username)) AS username,
u.account_status,
u.signup_date,
s.session_start
FROM users u
LEFT JOIN user_sessions s
ON u.user_id = s.user_id
ORDER BY username, session_startOne LEFT JOIN keeps every user; the cleaned username alias drives the sort.
- Inner join
JOIN user_sessionskeeps only users with at least one session. Every account that signed up and never came back is dropped, and the row count looks plausible enough that nobody checks.- Left join
LEFT JOIN user_sessionskeeps every row ofusers. The never-engaged accounts show up once with a NULLsession_start, which is exactly the population an onboarding or churn report exists to find.
Common follow-up questions
- How would you show a count of sessions per user instead of one row per session? (Group the joined rows and count `s.session_id`, which counts zero for session-less users where `COUNT(*)` would say one.)
- What if you wanted 'No sessions' instead of NULL in `session_start`? (Wrap the column in `COALESCE`, and notice it changes the type of the column to text.)
- Two different accounts clean to the same name. How would you tell them apart in the roster? (Tests whether the candidate brings `user_id` back as the real identity once the display name is no longer unique.)