The Conversion Story

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
L5
Asked in
Visa

Problem

Our referral-source conversion numbers don't reconcile, so we're rebuilding them from event_data, labelling each signup or purchase event by its raw tags text as the first of "organic", "referral", "campaign_a" and "campaign_b" that appears there with its double quotes, checked in that order, or 'other' when none does. For every referral_source with at least one signup event, report signup_users, purchase_users and funnel_users (unique users who signed up, purchased, or did either) plus conversion_rate, purchasers over signups rounded to 4 decimal places, with the sources in alphabetical order.

Signups are one thing. Paid purchases are another. Find the gap by source.

Tables

event_data

Columns: event_id INTEGER, event_type TEXT, user_id INTEGER, 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 4 of 4 rows
referral_sourcesignup_userspurchase_usersfunnel_usersconversion_rate
campaign_a1010
organic4150.25
other19109
referral1010

Worked solution and explanation

What this really is

This is a per-source people count wearing a tag-parsing costume. Everyone builds the source label; what separates candidates is counting users, not events, at three grains at once: who signed up, who paid, and who reached either step. Tally rows and a user who signed up twice under one source counts twice in the base. Skip the step filter because the conditional counts look self-filtering, and every user who merely browsed lands in funnel_users, so the columns stop reconciling with each other.

Build it in order

Step 1: Derive the source from tags

There is no referral_source column. Test the raw tags text with LIKE in priority order, organic first and other last, so an event carrying two tokens lands in the higher one. Keep the double quotes inside each pattern so campaign_a only matches the whole token, never a longer tag that happens to contain it. A NULL tags matches nothing and falls through to other.

Step 2: Keep only the two funnel steps

Filter to event_type IN ('signup', 'purchase') before counting anything. The signup and purchase columns would survive without it, but the either-step column would not: drop the filter and a user whose only events are page views and searches inflates funnel_users.

Step 3: Count people at three grains

COUNT(DISTINCT CASE WHEN event_type = 'signup' THEN user_id END) is the signup base, the same shape on 'purchase' is the payers, and a plain COUNT(DISTINCT user_id) over the filtered rows is everyone who reached either step. The CASE hands back NULL for the other step, and the count ignores it.

Step 4: Guard the base, then divide

HAVING drops a source with purchases but no signups, since it has nothing to convert from. The outer query divides the two named counts once, multiplying by 1.0 so integer division cannot floor the rate to zero, and rounds to 4 places.

The solution

Per-source signup, purchase and funnel users
WITH sourced_events AS (
  SELECT
    user_id,
    event_type,
    CASE
      WHEN tags LIKE '%"organic"%'    THEN 'organic'
      WHEN tags LIKE '%"referral"%'   THEN 'referral'
      WHEN tags LIKE '%"campaign_a"%' THEN 'campaign_a'
      WHEN tags LIKE '%"campaign_b"%' THEN 'campaign_b'
      ELSE 'other'
    END AS referral_source
  FROM event_data
  WHERE event_type IN ('signup', 'purchase')
),
source_funnel AS (
  SELECT
    referral_source,
    COUNT(DISTINCT CASE WHEN event_type = 'signup'   THEN user_id END) AS signup_users,
    COUNT(DISTINCT CASE WHEN event_type = 'purchase' THEN user_id END) AS purchase_users,
    COUNT(DISTINCT user_id) AS funnel_users
  FROM sourced_events
  GROUP BY referral_source
  HAVING COUNT(CASE WHEN event_type = 'signup' THEN 1 END) > 0
)
SELECT
  referral_source,
  signup_users,
  purchase_users,
  funnel_users,
  ROUND(purchase_users * 1.0 / signup_users, 4) AS conversion_rate
FROM source_funnel
ORDER BY referral_source;

The counts are named once in source_funnel; the final select only divides and sorts.

Common follow-up questions