Transaction Share of User Spend

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
Asked in
Etsy

Problem

We measure every purchase in transactions against everything its user has ever spent, yet each purchase keeps its own line instead of collapsing into one row per user. Show the transaction ID, the username from users, the amount and that share as a decimal, with usernames in alphabetical order and each user's purchases by ascending transaction ID.

Each transaction's share of the whole.

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

Expected output

First 5 of 200 rows
transaction_idusernametotal_amountspend_share
1134 amelia36.930.004101911337403033
2139 amelia238.980.026544131367792496
3144 amelia441.030.04898635139818196
4149 amelia643.080.07142857142857142
5154 amelia845.130.09387079145896088

Worked solution and explanation

This is a grain problem dressed up as a spend report. The numerator lives at the transaction grain and the denominator lives at the user grain, and the whole question is whether you can put both on one row without losing either. Everyone knows how to get SUM(total_amount) per user. The reflex is GROUP BY user_id, and the moment you write it, transaction_id and each row's total_amount are folded into 15 buyer rows. You cannot report a share of a purchase you just collapsed away.

Two grains on one row

Step 1: Attach the username

Inner-join transactions to users on user_id. Every buyer exists in users and user_id is unique there, so the join neither drops nor duplicates a purchase: 200 rows in, 200 rows out.

Step 2: Broadcast the user total

SUM(t.total_amount) OVER (PARTITION BY t.user_id) computes each user's all-time spend and hands that number to every one of their rows. There is no ORDER BY inside the window, so the frame is the whole partition. Add one and you silently get a running total instead.

Step 3: Divide at the row

t.total_amount * 1.0 / ... is the share. total_amount is REAL on this seed, so the * 1.0 is insurance here, not a fix. It is the habit that saves you the day the amount is stored as integer cents.

Step 4: Sort by what you return

Order by u.username, then t.transaction_id. Both are output columns, so the order can be checked from the rows alone. The leading space in ' amelia' sorts ahead of every letter, which is why that user opens the result.

Each purchase beside its user's total
SELECT
    t.transaction_id,
    u.username,
    t.total_amount,
    t.total_amount * 1.0 / SUM(t.total_amount) OVER (PARTITION BY t.user_id) AS spend_share
FROM transactions t
JOIN users u ON t.user_id = u.user_id
ORDER BY u.username, t.transaction_id
Collapse first
GROUP BY t.user_id with SUM(t.total_amount) returns 15 rows, one per buyer. The transaction IDs and amounts are gone, and getting them back means joining the totals onto transactions in a second pass.
Broadcast instead
SUM(...) OVER (PARTITION BY t.user_id) returns all 200 rows, each with its own amount next to its user's total. One pass and nothing to reassemble.

Common follow-up questions