User 360

A hard SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
hard
Seniority
L6

Problem

Each user in users can have many purchases in transactions and many visits in user_sessions. Produce one row per user, including users with no activity at all, with their number of transactions, total amount spent and total time on site in seconds, showing 0 wherever there is nothing to count. Next to every user goes the platform-wide average of total spend across the users who have transacted.

One row per user. Everything they did, or didn't do.

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 200 rows
usernametransaction_counttotal_spenttotal_session_secondsavg_spend_per_active_user
alice128605.98218409186.096
aaron42148814.5448659186.096
amelia149003.1249809186.096
beatrice127959.4260159186.096
brooke70064759186.096

Worked solution and explanation

What this really is

Strip the activity-summary framing and this is two independent one-to-many rollups hung off the same parent, plus one population-level benchmark. Both transactions and user_sessions carry many rows per user, so the tempting move is one flat query that LEFT JOINs users to both and sums. That query quietly pairs every one of a user's transactions with every one of their sessions before the SUM ever runs: total_spent comes back multiplied by the session count and total_session_seconds by the transaction count. And no, SUM(DISTINCT ...) does not save you, because amounts and durations repeat within a user, so de-duping throws away real rows. The only correct path is to collapse each table to one row per user first, then join, and to fetch the platform average with a scalar subquery, because a per-row view of the whole population cannot come out of the same grouping.


Build it

Step 1: Pre-aggregate transactions

SELECT user_id, COUNT(*) AS transaction_count, SUM(total_amount) AS total_spent FROM transactions GROUP BY user_id collapses the transaction rows to one row per user. Crucially the COUNT and SUM happen here, before any join, so each transaction is seen exactly once.

Step 2: Pre-aggregate sessions

SELECT user_id, SUM(session_duration_sec) AS total_session_seconds FROM user_sessions GROUP BY user_id does the same for sessions, one row per user with the true total time on site.

Step 3: Left join from users to both aggregates

Drive off users and LEFT JOIN both aggregates on user_id. LEFT JOIN keeps users with no transactions or no sessions; COALESCE turns the resulting NULLs into 0 so every user, including the ones who never acted, reads as a real number.

Step 4: Attach the population benchmark

The benchmark is one number for the whole result, so it belongs in a scalar subquery: (SELECT AVG(user_total) FROM (SELECT SUM(total_amount) AS user_total FROM transactions GROUP BY user_id)). Total each transacting user's spend first, then average those totals. It is NOT AVG(total_amount) over raw transactions, which averages purchases, not people.


The solution

Pre-aggregate each fact table, join, then benchmark
SELECT
    u.username,
    COALESCE(t.transaction_count, 0) AS transaction_count,
    COALESCE(t.total_spent, 0) AS total_spent,
    COALESCE(s.total_session_seconds, 0) AS total_session_seconds,
    (
        SELECT AVG(user_total)
        FROM (
            SELECT SUM(total_amount) AS user_total
            FROM transactions
            GROUP BY user_id
        )
    ) AS avg_spend_per_active_user
FROM users u
LEFT JOIN (
    SELECT user_id, COUNT(*) AS transaction_count, SUM(total_amount) AS total_spent
    FROM transactions
    GROUP BY user_id
) t ON u.user_id = t.user_id
LEFT JOIN (
    SELECT user_id, SUM(session_duration_sec) AS total_session_seconds
    FROM user_sessions
    GROUP BY user_id
) s ON u.user_id = s.user_id

Common follow-up questions