The Space Between

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
L4
Asked in
Meta

Problem

An onboarding team measures user momentum as the average gap, in seconds, between each of a user's events and the one immediately before it, where event_timestamp is stored as a Unix epoch second. For each user with more than one recorded event, return that average gap, ignoring events with no user attached.

The time between events is the tell.

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 5 of 74 rows
user_idavg_progression_seconds
1004
1973
2942
3911
4880

Worked solution and explanation

What this problem really is

Strip the onboarding language and this is a per-user consecutive difference: the gap from each event to the one before it, averaged inside each user. Because event_timestamp is already a Unix epoch second, the gap is a plain subtraction, so no one loses points on date parsing. The real skill is the window. Leave off PARTITION BY user_id and the look-back computes gaps across strangers, so a user's first event borrows some other user's last timestamp. The query still runs and still returns numbers, which is exactly what makes it dangerous in an interview.


Break down the requirements

Step 1: Look back within each user

Use LAG over event_data partitioned by user_id and ordered by event_timestamp (with event_id as a tiebreaker) to pull each event's preceding timestamp for that same user. Filter out rows where user_id IS NULL first, because a missing user is not a user and would otherwise form its own bogus partition.

Step 2: Take the gap

Compute event_timestamp - prev_timestamp. Both are epoch seconds, so the difference is already the number of seconds between the two events. No cast, no date parsing, no unit juggling.

Step 3: Drop each user's first event

Keep only rows where prev_timestamp IS NOT NULL. That drops every user's first event, and a user with a single event yields zero gap rows and disappears entirely, which is precisely the 'more than one event' filter, for free.

Step 4: Average per user

GROUP BY user_id and AVG the per-event gaps. The average is taken only over the surviving gap rows, so users who never produced a gap contribute nothing.

Step 5: Order for the team

ORDER BY user_id at the outer layer for deterministic output. The window's own sort is internal to each partition, not the final ordering.


The solution

PER-USER AVG GAP VIA LAG
WITH event_gaps AS (
  SELECT
    user_id,
    event_timestamp,
    LAG(event_timestamp) OVER (
      PARTITION BY user_id ORDER BY event_timestamp, event_id
    ) AS prev_timestamp
  FROM event_data
  WHERE user_id IS NOT NULL
)
SELECT
  user_id,
  AVG(event_timestamp - prev_timestamp) AS avg_progression_seconds
FROM event_gaps
WHERE prev_timestamp IS NOT NULL
GROUP BY user_id
;

COMMON FOLLOW-UP QUESTIONS

Common follow-up questions