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

users: 10 of 200 rows
user_idusernameemailsignup_dateaccount_statusage_bucket
100alicealice@example.com2025-02-02inactive25-34
197aaron42aaron42@example.com2026-03-03suspended35-44
294 ameliaamelia@example.com2024-04-04pending_verification45-54
391arjun arjun@example.com2025-05-05active55-64
488ava99ava99@example.com2026-06-06inactive65+
585andrew_kandrew_k@example.com2024-07-07suspendedNULL
682anikaanika@example.com2025-08-08 16:34:00pending_verification18-24
779aidenaiden@example.com2026-09-09 14:53:00active25-34
876aria_baria_b@example.com2024-10-10inactive35-44
973brianbrian@example.com2025-11-11suspended45-54

user_sessions

Columns: session_id INTEGER, user_id INTEGER, session_start TEXT, device_id INTEGER, session_duration_sec INTEGER, pages_viewed INTEGER

user_sessions: 10 of 200 rows
session_iduser_iddevice_idsession_startsession_duration_secpages_viewed
70611972302026-02-02 01:00:00533
71222942592026-03-03 02:00:00766
71833912882026-04-04 03:00:00999
72444883172026-05-05 04:00:0012212
73055853462026-06-06 05:00:0014515
73666823752026-07-07 06:00:0016818
74277794042026-08-08 07:00:0019121
7488876NULL2026-09-09 08:00:0021424
75499734622026-01-10 09:00:0023727
761010704912026-02-11 10:00:0026030

transactions

Columns: transaction_id INTEGER, user_id INTEGER, total_amount REAL, product_id INTEGER, quantity INTEGER, transaction_date TEXT

transactions: 10 of 200 rows
transaction_iduser_idproduct_idquantitytotal_amounttransaction_date
10671971001223.462026-02-02
11342941050336.932026-03-03
12013911099450.42026-04-04
12684881148563.872026-05-05
13355851197002026-06-06
14026821246290.812026-07-07
146977912953104.282026-08-08
153687613444117.752026-09-09
160397313935-452026-01-10
1670107014421144.692026-02-11

Expected output

First 5 of 40 rows
usernameaccount_statuslast_loginlifetime_spend
calebactive2026-01-05 10:00:000
divyaactive2026-02-05 10:00:000
dora_77active2026-03-05 10:00:000
eduardoactiveNULL0
felixactiveNULL0

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

Outer joins, a status filter, a test on the latest login
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 NULL

Common follow-up questions