Active Duo

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

The growth team is building a cross-engagement segment of users who both make purchases and log browsing sessions on the platform. Return an alphabetical roster of the usernames for users with activity in both areas.

Shoppers who also browse. The overlap is the insight.

Tables

users

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

transactions

Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, 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

user_sessions

Columns: session_id INTEGER, user_id INTEGER, device_id INTEGER, session_start TEXT, 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

Expected output

First 5 of 15 rows
username
amelia
aaron42
aiden
alice
andrew_k

Worked solution and explanation

What this really is

This is a set intersection dressed up as a growth segment. A user belongs if they appear in transactions and in user_sessions, and those two tables share nothing but user_id. Anyone can write the three-way join. The trick is seeing what it returns: every purchase paired with every session of the same user, so the rows are purchase-session pairs, not people. On this data that is 1,036 rows for 15 users, between 60 and 96 copies of each name, and a roster with alice on it 96 times is not a roster.

Step 1: Fix the grain before joining

One row per user. Say it first, because it decides everything after: whatever you read from transactions or user_sessions may only confirm a user, never add rows for them.

Step 2: Require both activities on user_id

Inner-join users to transactions and to user_sessions, both on u.user_id. An inner join drops any user missing from either side, which is exactly the both-areas rule. Reading user_sessions alone would admit 59 users; only 15 of them ever bought anything.

Step 3: Collapse the fan-out

Each surviving user now appears once per purchase-session pair, roughly 13 purchases times 5 sessions here. SELECT DISTINCT u.username folds those back to one row each. It is safe because no two of the 15 qualifying users share a username.

Step 4: Sort the roster as stored

ORDER BY u.username gives the A to Z list. Two usernames are stored with stray spaces (' amelia' and 'arjun '), and the roster returns them untouched, which is why ' amelia' sorts ahead of 'aaron42': a space comes before every letter.

The solution

Distinct-filter for active duo
SELECT DISTINCT u.username
FROM users u
JOIN transactions t ON u.user_id = t.user_id
JOIN user_sessions s ON u.user_id = s.user_id
ORDER BY u.username
The version that looks finished
SELECT u.username
FROM users u
JOIN transactions t ON u.user_id = t.user_id
JOIN user_sessions s ON u.user_id = s.user_id
ORDER BY u.username

Returns 1,036 rows: every purchase of a user paired with every one of their sessions.

Join both, then deduplicate
Builds every purchase-session pair per user, then throws the duplicates away with DISTINCT. It is correct and short, and it is the version most interviewers expect first.
Two existence checks
WHERE EXISTS (SELECT 1 FROM transactions t WHERE t.user_id = u.user_id) AND EXISTS (...) asks each user two yes or no questions and stops at the first match in each table. No pairs are built, so nothing needs deduplicating.

Common follow-up questions