App Stability by Region

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

The mobile team is weighing the next rollout and wants to see where the app keeps falling over. Looking only at app opens and crashes, count each for every tag set and event timestamp, and report the crash rate as crashes divided by opens, rounded to four decimal places.

Some regions crash more than others.

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 26 rows
event_timestamptagsnum_opensnum_crashescrash_rate
2022-03-24 03:57:00["mobile", "android", "free", "campaign_a", "referral"]01NULL
2022-12-09 12:12:00["ios", "premium"]100
2023-01-10 13:19:00["android", "free", "campaign_a"]01NULL
2023-10-23 22:34:00["premium", "new_user", "organic", "beta"]100
2024-11-24 23:41:00["free", "campaign_a", "referral", "experiment_v1", "desktop"]01NULL

Worked solution and explanation

Why this problem exists in real interviews

This is conditional aggregation wearing a reliability costume. The real question: can you pivot two event types into side-by-side counts and divide them in one pass, without the integer-division trap eating your ratio? Anyone can filter to opens and crashes. What separates candidates is forcing float division and deciding what a bucket with zero opens should return. Miss the first and a real 17 percent crash rate rounds to 0; miss the second and the whole query dies on the first tag set that never opened.


Break down the requirements

Step 1: Filter to the two event types

WHERE event_type IN ('open', 'crash') strips the rest before aggregation. Doing it in WHERE (not inside the CASE) shrinks what the GROUP BY hashes, especially given the 500M-row scale.

Step 2: Pivot via conditional SUM

Two SUM(CASE WHEN event_type = 'open' THEN 1 ELSE 0 END) expressions turn rows into wide counts. COUNT(CASE WHEN ... THEN 1 END) works too; both ignore the other event type cleanly.

Step 3: Group by the bucket keys

GROUP BY event_timestamp, tags matches the grain in the output preview: each distinct (timestamp, tag set) pair is one row. Keep the timestamp raw and do not truncate it to a calendar day, because the preview shows full timestamps with their time components.

Step 4: Compute the ratio safely

crashes * 1.0 / opens forces float division, then ROUND(..., 4). If a bucket can have zero opens, this divides by zero; wrap the denominator in NULLIF(..., 0) to return NULL instead of erroring.


The solution

CONDITIONAL-AGG PIVOT FOR CRASH RATE
SELECT
  event_timestamp,
  tags,
  SUM(CASE WHEN event_type = 'open'  THEN 1 ELSE 0 END) AS num_opens,
  SUM(CASE WHEN event_type = 'crash' THEN 1 ELSE 0 END) AS num_crashes,
  ROUND(
    SUM(CASE WHEN event_type = 'crash' THEN 1 ELSE 0 END) * 1.0
    / SUM(CASE WHEN event_type = 'open' THEN 1 ELSE 0 END),
    4
  ) AS crash_rate
FROM event_data
WHERE event_type IN ('open', 'crash')
GROUP BY event_timestamp, tags
Long format: GROUP BY `event_timestamp`, tags, `event_type`
One row per event type per bucket. Opens and crashes land on separate rows, so a consumer has to pivot before it can divide. More rows, more downstream work.
Wide format: conditional SUM
Two SUM(CASE WHEN ...) expressions put num_opens and num_crashes side by side in one row, so crash_rate is computable inline. This is the shape the output preview asks for.

COMMON FOLLOW-UP QUESTIONS

Common follow-up questions