Fastest Page View to Click
A hard SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- hard
- Seniority
- L5
- Asked in
- Meta
Problem
Return the three quickest browse-to-click transitions in event_data, from the shortest gap to the longest with a tied gap going to the earlier browsing event, each with that event's type and user_id, both timestamps, the gap in seconds, and the average gap across every transition in the stream. A transition is a 'page_view', 'search' or 'open' event whose very next event in the whole stream, from any user and of any type, is a 'button_click'.
How fast from view to click?
Tables
event_data
Columns: event_id INTEGER, event_type TEXT, user_id INTEGER, 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 | browse_event | browse_time | click_time | gap_seconds | avg_gap_seconds |
|---|---|---|---|---|---|
| NULL | page_view | 2026-07-15 22:10:00 | 2026-07-16 15:45:00 | 63300.0000089407 | 6749963.999994099 |
| NULL | page_view | 2026-02-01 04:16:00 | 2026-02-02 01:07:00 | 75059.99999195337 | 6749963.999994099 |
| NULL | page_view | 2026-03-01 08:32:00 | 2026-03-02 05:23:00 | 75059.99999195337 | 6749963.999994099 |
Worked solution and explanation
What this really is
This is a one-step lookahead over a shared stream, dressed up as engagement analytics. Everyone reaches for LEAD. What separates candidates is deciding what LEAD is allowed to see. 'Next' means the literal next event, any type, any user. Filter the stream down to browse and click events first and a click that came after a purchase suddenly pairs with an earlier search. Skip the browse-type check and every login or error sitting before a click joins the podium. Either way the three rows and the average describe the wrong events.
Building it
Step 1: Look ahead over the whole stream
In event_stream, take two LEAD lookups over the same ORDER BY event_timestamp: one for next_type, one for next_ts. No WHERE here and no PARTITION BY user_id: the stream is shared, and an intervening purchase has to stay visible so it can block a pair.
Step 2: Keep browse events that a click follows
In transitions, keep rows whose event_type is one of the three browsing types and whose next_type is 'button_click'. The user_id carried along is the browser's, which is what the output asks for.
Step 3: Turn the difference into seconds
(JULIANDAY(next_ts) - JULIANDAY(event_timestamp)) * 86400 gives elapsed seconds and survives midnight and year boundaries. Forget the * 86400 and a two-second gap reads as a tiny fraction of a day.
Step 4: Average the whole set, then cut to three
(SELECT AVG(gap_seconds) FROM transitions) reads the full CTE, so it ignores the LIMIT 3 on the outer query. Sort by gap_seconds ascending, let the earlier browse_time win a tied gap, and keep three rows.
WITH event_stream AS (
SELECT user_id,
event_type,
event_timestamp,
LEAD(event_type) OVER (ORDER BY event_timestamp) AS next_type,
LEAD(event_timestamp) OVER (ORDER BY event_timestamp) AS next_ts
FROM event_data
),
transitions AS (
SELECT user_id,
event_type AS browse_event,
event_timestamp AS browse_time,
next_ts AS click_time,
(JULIANDAY(next_ts) - JULIANDAY(event_timestamp)) * 86400 AS gap_seconds
FROM event_stream
WHERE event_type IN ('page_view', 'search', 'open')
AND next_type = 'button_click'
)
SELECT user_id,
browse_event,
browse_time,
click_time,
gap_seconds,
(SELECT AVG(gap_seconds) FROM transitions) AS avg_gap_seconds
FROM transitions
ORDER BY gap_seconds ASC, browse_time ASC
LIMIT 3- Filter the stream first
- Putting the
INlist insideevent_streamhides purchases and logins fromLEAD. A search, then a purchase, then a click becomes a transition it never was, and its gap lands in the average. - Filter the starting row
LEADsees every event, so an intervening purchase blocks the pair. TheINlist then only decides which rows may start a transition.
Common follow-up questions
- Product decides a transition only counts when the browser and the clicker are the same user. What changes? (Adding `PARTITION BY user_id` to both windows, and noticing the average shifts because pairs change.)
- A click five days after a search is technically next. How would you exclude stale pairs? (A max-gap filter on `gap_seconds` and whether it belongs before or after the average.)
- Report the median gap per browsing type instead of one overall average. How does the benchmark change? (Grouping `transitions` by `browse_event` and picking the middle value per group.)