The Shape of a User

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
L6
Asked in
Lyft

Problem

Produce the product analytics team's per-user funnel snapshot from event_data: one row per user, with separate columns counting that user's search, add_to_cart, and purchase events.

Every user leaves a trail of events. The report needs them lined up side by side.

Tables

event_data

Columns: event_id INTEGER, user_id INTEGER, event_type TEXT, event_timestamp TEXT, payload TEXT, tags TEXT, properties TEXT

event_data: 10 of 200 rows
event_idevent_typeuser_idevent_timestamppayloadtagsproperties
3059button_click1002026-02-02 01:07:00{"button_id": "cancel", "element_class": "btn-primary", "position": {"x": 13, "y": 7}}["desktop", "web", "returning"]{"os": "Edge", "language": "US", "screen_size": "en", "utm_source": "mobile"}
3118purchase1972026-03-03 02:14:00{"amount": 56.93, "currency": "GBP", "items": [{"sku": "SKU1002", "qty": 3}]}["ios", "premium", "new_user", "organic"]{"country": "Windows", "device_type": "FR", "referrer": "fr", "utm_medium": "1920x1080", "browser": "twitter"}
3177signup2942026-04-04 03:21:00{"signup_method": "apple", "referral_code": "REF3X", "accepted_terms": true}["android", "free", "campaign_a", "referral", "experiment_v1"]{"language": "iOS", "screen_size": "es", "utm_source": "desktop", "session_id": "bing", "os": "organic"}
3236login3912026-05-05 04:28:00{"login_method": "password", "remember_me": true, "mfa_used": false}["web", "returning"]{"device_type": "JP", "referrer": "ja", "utm_medium": "1440x900"}
3295logout4882026-06-06 05:35:00{"action": "logout", "value": 50}["premium", "new_user", "organic"]{"screen_size": "de", "utm_source": "tablet", "session_id": "amazon", "os": "Chrome"}
3354search5852026-07-07 06:42:00{"query": "phone", "results_count": 102, "filters": {"category": "electronics"}}["free", "campaign_a", "referral", "experiment_v1"]{"referrer": "mobile", "utm_medium": "375x812", "browser": "email", "country": "Edge", "device_type": "US"}
3413add_to_cartNULL2026-08-08 07:49:00{"product_id": 1007, "quantity": 2, "price": 58.49, "variant": "medium"}["returning", "campaign_b", "featured", "experiment_v2", "ios"]{"utm_source": "fr", "session_id": "twitter", "os": "Safari", "language": "Windows", "screen_size": "FR"}
3472open7792026-09-09 08:56:00{"action": "open", "value": 80}["new_user", "organic"]{"utm_medium": "bing", "browser": "organic", "country": "iOS"}
3531crash8762026-01-10 09:03:00{"action": "crash", "value": 90}["campaign_a", "referral", "experiment_v1"]{"session_id": "direct", "os": "Firefox", "language": "macOS", "screen_size": "JP"}
3590error9732026-02-11 10:10:00{"action": "error", "value": 100}["campaign_b", "featured", "experiment_v2", "ios"]{"browser": "Chrome", "country": "Android", "device_type": "DE", "referrer": "de", "utm_medium": "tablet"}

Expected output

First 5 of 92 rows
user_idsearchesadd_to_cartspurchases
NULL0130
100000
585200
1943200
8733100

Worked solution and explanation

What this really is

This is a long-to-wide reshape wearing an analytics-report costume. The skill under test: can you collapse many event rows per user into one row with a fixed set of typed columns, using conditional aggregation, because standard SQL has no runtime PIVOT that invents column names for you. Anyone can write GROUP BY user_id. The move that separates people is putting the event_type test INSIDE the aggregate, not in a WHERE clause. Filter the type in WHERE and you keep only one kind of event per pass and throw the rest away: a user who searched and bought loses one of their two columns, and the whole point of the report is gone.


Walk the requirements

Step 1: Fold events down to one row per user

GROUP BY user_id gives exactly one output row per user. The null user_id (an unattributed event) folds into its own group and is kept, which is why a row with a blank user appears in the result.

Step 2: Count each type conditionally, in one pass

For each type you want as a column, write SUM(CASE WHEN event_type = 'search' THEN 1 ELSE 0 END) AS searches, and repeat for add_to_cart and purchase. The CASE evaluates per row inside the group, so all three counts come out of a single pass over the user's events.

Filter the type in WHERE
WHERE event_type = 'purchase' scopes the WHOLE query to purchases. You can only produce one type's column per run, and the same user's searches and add_to_cart events are discarded before the GROUP BY sees them.
Test the type inside the aggregate
SUM(CASE WHEN event_type = 'purchase' ...) keeps every row in scope and decides per column what to count. One scan yields all three columns, and every user appears once with the right split.

The solution

Conditional-aggregation reshape
SELECT
    user_id,
    SUM(CASE WHEN event_type = 'search' THEN 1 ELSE 0 END) AS searches,
    SUM(CASE WHEN event_type = 'add_to_cart' THEN 1 ELSE 0 END) AS add_to_carts,
    SUM(CASE WHEN event_type = 'purchase' THEN 1 ELSE 0 END) AS purchases
FROM event_data
GROUP BY user_id

Common follow-up questions