Average Compensation by Department and Status

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 comparing how much value different kinds of accounts bring in. For each account status, find the average total value per user, where a user's value is everything they spent on purchases plus the ad revenue their impressions earned. Users who never purchased or never earned ad revenue still count, contributing zero from whichever side they lack.

Average compensation. Department by department.

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

cost_allocs

Columns: alloc_id INTEGER, team_name TEXT, svc_name TEXT, category TEXT, amount REAL, period TEXT, region TEXT, acct_id TEXT

cost_allocs: 10 of 200 rows
alloc_idteam_namesvc_nameregionamountperiodacct_idcategory
129data-engS3us-west-283.212026-02acct-101storage
158ml-teamRDSeu-west-1156.422026-03acct-102network
187frontendLambdaap-south-1229.632026-04acct-103database
216backendBigQueryus-central1-120.52026-05acct-104ml
245devopsCloudFronteurope-west1376.052026-06acct-105other
274securityEKSap-southeast-1449.262026-07acct-106compute
303mobileSQSeu-central-1522.472026-08acct-107storage
332PlatformEC2us-east-1595.682026-09NULLnetwork
361DATA-ENGS3us-west-2668.892026-01acct-109database
390platformRDSeu-west-1742.12026-02acct-110ml

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

ad_impressions

Columns: impression_id INTEGER, user_id INTEGER, ad_campaign TEXT, impression_time TEXT, clicked INTEGER, revenue REAL

ad_impressions: 10 of 200 rows
impression_iduser_idad_campaignimpression_timeclickedrevenue
253100BRAND_AWARENESS_Q12026-02-02 01:07:000NULL
306197PRODUCT_LAUNCH_X2026-03-03 02:14:000NULL
359294RETARGETING_CART2026-04-04 03:21:000NULL
412391HOLIDAY_PROMO2026-05-05 04:28:000NULL
465488NEW_USER_ACQU2026-06-06 05:35:0010.2
518NULLLOYALTY_PROGRAM2026-07-07 06:42:000NULL
571682FLASH_SALE_48H2026-08-08 07:49:000NULL
624779SUMMER_SALE_20242026-09-09 08:56:000NULL
677876BRAND_AWARENESS_Q12026-01-10 09:03:000NULL
730973PRODUCT_LAUNCH_X2026-02-11 10:10:0010.35

Expected output

First 4 of 4 rows
account_statusavg_total_value
active543.0738
inactive726.4004000000001
pending_verification755.4372
suspended736.0114

Worked solution and explanation

What this really is

Underneath the finance framing this is a per-user rollup of two sparse fact tables, stitched back onto the full user list and made null-safe before anything is averaged. Each user's value is their spend in transactions plus their earnings in ad_impressions, and the whole difficulty is that most users are missing one side or the other: only a handful ever transacted, and most impressions carry no revenue at all. Inner-join the facts and you quietly drop every user who never bought or never earned ad revenue. Add a raw NULL sum and that user's total collapses to NULL and vanishes from the AVG. Either slip averages only your most active users, so every account status looks far richer than it is.

Two sparse fact tables hang off `users`; a user may have rows in neither, one, or both.
  • users: user_id INTEGER PK, account_status TEXT
  • transactions: transaction_id INTEGER PK, user_id INTEGER FK, total_amount REAL
  • ad_impressions: impression_id INTEGER PK, user_id INTEGER FK, revenue REAL
  • transactions references users
  • ad_impressions references users

The trap

Inner join, raw sums
Inner-join the two fact sides and add them directly. A user with no ad revenue never appears, and a user with an impression but NULL revenue nulls their whole total. Both silently leave the average.
Outer join, coalesce to 0
Keep every user by anchoring on users, take spend as a per-user sum and revenue through a LEFT JOIN, then COALESCE(..., 0) each side. A missing side contributes 0, the user stays in, and the AVG runs over the true population.

Building it

Step 1: Total each user's spend as a per-user sum

Compute each user's spend with a scalar sum over transactions scoped to that user: (SELECT SUM(t.total_amount) FROM transactions t WHERE t.user_id = u.user_id). The WHERE t.user_id = u.user_id correlates the sum to the one user the outer row is about, so a user with several purchases gets a single correct total with no fan-out, and a user with none returns NULL rather than disappearing.

Step 2: Attach ad revenue without dropping anyone

Pre-aggregate ad_impressions to one row per user (SUM(revenue) ... GROUP BY user_id) and LEFT JOIN that rollup onto the user set. The LEFT JOIN is load-bearing: an inner join would drop every user with no ad revenue, which is most of them, and pull each status average upward.

Step 3: Neutralize the nulls, then average by status

COALESCE(..., 0) each side before adding, so a missing spend or a missing revenue contributes 0 instead of nulling the whole total and evicting the user from the AVG. Anchor the query on users for the full population and its account_status, then group by status and average the per-user totals.

Average total value per account status
SELECT u2.account_status,
       AVG(
         COALESCE((SELECT SUM(t.total_amount)
                   FROM transactions t
                   WHERE t.user_id = u.user_id), 0)
         + COALESCE(a.total_rev, 0)
       ) AS avg_total_value
FROM users u
JOIN users u2 ON u2.user_id = u.user_id
LEFT JOIN (
  SELECT user_id, SUM(revenue) AS total_rev
  FROM ad_impressions
  GROUP BY user_id
) a ON a.user_id = u.user_id
GROUP BY u2.account_status

Per-user spend as a correlated sum, ad revenue via an outer-joined rollup, both coalesced, then averaged by status.

Common follow-up questions