Beyond the Signup

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
L4

Problem

A month in which every session in user_sessions came from a different person shows no return visits, so the analyst tracking engagement drops it and keeps only months where someone logged more than one session. For each remaining month in the two years leading up to 2026-09-12, report how many users were active and the average session duration.

Anyone can create an account. Fewer actually return.

Tables

users

Columns: user_id INTEGER, signup_date TEXT, account_status TEXT, email TEXT, age_bucket TEXT, username 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, session_duration_sec INTEGER, pages_viewed INTEGER, device_id 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

Expected output

First 5 of 5 rows
monthactive_usersavg_duration_sec
2024-1251552.4285714285713
2025-0351486
2025-0751548
2025-1151636.5714285714287
2026-06151995.1176470588234

Worked solution and explanation

What this is really asking

This is a monthly rollup whose filter is built out of the very confusion it tests. Anyone can bucket session_start by month. The trap is the word 'active': one user_id can log several sessions in a month, so **COUNT(*) counts sessions, not people**. The month filter turns that trap inside out: 'someone came back' is a fact about the whole month, never about one session row, so it cannot live in WHERE. Miss either half and you keep months nobody returned in, while reporting session totals as people in the ones you keep.

Note that 'active' never touches users. A user is active in a month when one of their sessions falls in it, so user_sessions is the only table the answer reads.


Build it in three moves

Step 1: Cut the window in WHERE

Keep rows where session_start >= date('2026-09-12', '-24 months'). The cutoff rolls back two years from the anchor date, so the oldest bucket is a partial month. Each session either falls in the window or it does not, which makes it a row filter: it belongs in WHERE, before any grouping.

Step 2: Bucket by month, two aggregates side by side

Group on strftime('%Y-%m', session_start) so every session in a month lands in one bucket. Per bucket, COUNT(DISTINCT user_id) gives the people and AVG(session_duration_sec) gives the session-weighted average; NULL durations simply drop out of it.

Step 3: Keep months with a return visit in HAVING

If every session in a month came from a different person, COUNT(*) equals COUNT(DISTINCT user_id). One person logging twice pushes the session count above the people count. So HAVING COUNT(*) > COUNT(DISTINCT user_id) keeps exactly the months someone came back.


The solution

Active users in months someone came back
SELECT strftime('%Y-%m', us.session_start) AS month,
       COUNT(DISTINCT us.user_id) AS active_users,
       AVG(us.session_duration_sec) AS avg_duration_sec
FROM user_sessions us
WHERE us.session_start >= date('2026-09-12', '-24 months')
GROUP BY month
HAVING COUNT(*) > COUNT(DISTINCT us.user_id)

WHERE filters sessions, HAVING filters months; the filter is the gap between the two counts.

Counts sessions
COUNT(*) AS active_users counts every session row, so in every month the filter keeps, the figure is too high by exactly the repeat sessions.
Counts people
COUNT(DISTINCT user_id) collapses each user to one per month, and the raw COUNT(*) stays useful as the other side of the HAVING comparison.
The tempting version
SELECT strftime('%Y-%m', session_start) AS month,
       COUNT(*) AS active_users,
       AVG(session_duration_sec) AS avg_duration_sec
FROM user_sessions
WHERE session_start >= date('2026-09-12', '-24 months')
GROUP BY month

Reports session totals as active_users and keeps months nobody returned in.


Common follow-up questions

Common follow-up questions