The Dormant Accounts
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
Finance is cleaning up active accounts that have gone dark, deciding whether to nudge them or shut them down: the ones whose most recent login was over 90 days ago, and the ones who have never logged in at all. For each, show the username, account status, the date of their last login, and their lifetime spend.
They are still paying. They stopped showing up.
Tables
users
Columns: user_id INTEGER, username TEXT, account_status TEXT, email TEXT, signup_date 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, session_start TEXT, device_id INTEGER, 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, total_amount REAL, product_id INTEGER, quantity INTEGER, 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
| username | account_status | last_login | lifetime_spend |
|---|---|---|---|
| caleb | active | 2026-01-05 10:00:00 | 0 |
| divya | active | 2026-02-05 10:00:00 | 0 |
| dora_77 | active | 2026-03-05 10:00:00 | 0 |
| eduardo | active | NULL | 0 |
| felix | active | NULL | 0 |
Worked solution and explanation
What this problem really is
This is a per-account rollup over two optional child tables, dressed up as an account-cleanup report. Every active account has to reach the end whether or not it has sessions or purchases, and dormancy is a fact about the account's latest login, not about any one session. Anyone can write the joins. What separates candidates is that the two cohorts finance most wants are exactly the ones a careless query deletes. Require a matching session and every never-logged-in account vanishes. Require a purchase and, on this data, the report comes back empty, because not one dormant account ever bought anything. Test the 90-day cutoff on session rows instead of the latest one and an account that logged in last week sneaks in on the strength of an old session.
Break down the requirements
Step 1: Outer-join both child tables onto users
Start at users, LEFT JOIN user_sessions on user_id for login timestamps, then LEFT JOIN transactions on user_id for spend. Either side can be missing for an account finance cares about, so neither join may be allowed to drop a user.
Step 2: Filter to active accounts
Apply WHERE u.account_status = 'active' so the cohort is locked in before aggregation. It is a per-row test, so it belongs in WHERE, not HAVING.
Step 3: Aggregate per user
Group by u.user_id, u.username, u.account_status. Project MAX(s.session_start) AS last_login and COALESCE(SUM(t.total_amount), 0) AS lifetime_spend. A sum over no rows is NULL; the COALESCE is what reports a never-spent account as 0.
Step 4: Apply the dormancy filter in HAVING
Use HAVING MAX(s.session_start) < datetime('now', '-90 days') OR MAX(s.session_start) IS NULL. The first branch catches accounts that logged in long ago; the second catches accounts that never logged in, whose MAX(session_start) is NULL because the outer join found no session. Drop that branch and they vanish, because NULL < anything is not true.
The solution
SELECT
u.username,
u.account_status,
MAX(s.session_start) AS last_login,
COALESCE(SUM(t.total_amount), 0) AS lifetime_spend
FROM users u
LEFT JOIN user_sessions s ON u.user_id = s.user_id
LEFT JOIN transactions t ON u.user_id = t.user_id
WHERE u.account_status = 'active'
GROUP BY u.user_id, u.username, u.account_status
HAVING MAX(s.session_start) < datetime('now', '-90 days')
OR MAX(s.session_start) IS NULLCommon follow-up questions
- How would you rewrite this so lifetime spend stays correct for accounts that have both sessions and purchases? (Tests pre-aggregating each child table per user before joining.)
- Finance wants to exclude accounts created in the last 90 days so fresh signups don't look dormant. How would you add that? (Tests adding a `signup_date` guard alongside the status filter.)
- If this needs to run hourly on 10M users, what would you precompute or cache to keep it cheap? (Tests maintaining a per-user last-login and spend table incrementally.)