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_id | event_type | user_id | event_timestamp | payload | tags | properties |
|---|---|---|---|---|---|---|
| 3059 | button_click | 100 | 2026-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"} |
| 3118 | purchase | 197 | 2026-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"} |
| 3177 | signup | 294 | 2026-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"} |
| 3236 | login | 391 | 2026-05-05 04:28:00 | {"login_method": "password", "remember_me": true, "mfa_used": false} | ["web", "returning"] | {"device_type": "JP", "referrer": "ja", "utm_medium": "1440x900"} |
| 3295 | logout | 488 | 2026-06-06 05:35:00 | {"action": "logout", "value": 50} | ["premium", "new_user", "organic"] | {"screen_size": "de", "utm_source": "tablet", "session_id": "amazon", "os": "Chrome"} |
| 3354 | search | 585 | 2026-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"} |
| 3413 | add_to_cart | NULL | 2026-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"} |
| 3472 | open | 779 | 2026-09-09 08:56:00 | {"action": "open", "value": 80} | ["new_user", "organic"] | {"utm_medium": "bing", "browser": "organic", "country": "iOS"} |
| 3531 | crash | 876 | 2026-01-10 09:03:00 | {"action": "crash", "value": 90} | ["campaign_a", "referral", "experiment_v1"] | {"session_id": "direct", "os": "Firefox", "language": "macOS", "screen_size": "JP"} |
| 3590 | error | 973 | 2026-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
| user_id | searches | add_to_carts | purchases |
|---|---|---|---|
| NULL | 0 | 13 | 0 |
| 100 | 0 | 0 | 0 |
| 585 | 2 | 0 | 0 |
| 1943 | 2 | 0 | 0 |
| 8733 | 1 | 0 | 0 |
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 andadd_to_cartevents are discarded before theGROUP BYsees 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
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_idCommon follow-up questions
- The team now wants `add_to_cart`-to-purchase conversion per user as a fourth column. How do you add it, and what breaks if a brand new event type appears next week? (Tests whether the candidate can extend the reshape without rewriting it, and whether they recognize that new column names cannot be produced dynamically in standard SQL.)
- With 12,000,000 distinct users, where does the time go in this query, and how would a materialized per-user rollup change the plan? (Probes understanding of how `user_id` cardinality drives the group-and-sort cost and where an index or rollup actually helps.)
- Events like 'login' and 'crash' never appear in any column. Is that intended, and how would you surface a catch-all 'other' count if asked? (Tests awareness that the `CASE` branches are not exhaustive and that the reshape silently ignores unlisted types.)