Fault Lines

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
Meta

Problem

The reliability team is building a daily failure report and treats each user's age_bucket as a stand-in for region. Looking only at open, error, and crash events, give them for every day and region the number of error and crash events as a share of the number of opens.

Errors by day and region. Some areas are worse than they appear.

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"}

users

Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT

users: 10 of 200 rows
user_idusernameemailsignup_dateaccount_statusage_bucket
100alicealice@example.com2025-02-02inactive25-34
197aaron42aaron42@example.com2026-03-03suspended35-44
294 ameliaamelia@example.com2024-04-04pending_verification45-54
391arjun arjun@example.com2025-05-05active55-64
488ava99ava99@example.com2026-06-06inactive65+
585andrew_kandrew_k@example.com2024-07-07suspendedNULL
682anikaanika@example.com2025-08-08 16:34:00pending_verification18-24
779aidenaiden@example.com2026-09-09 14:53:00active25-34
876aria_baria_b@example.com2024-10-10inactive35-44
973brianbrian@example.com2025-11-11suspended45-54

Expected output

First 5 of 37 rows
event_dayregionerror_rate
2022-03-2435-44NULL
2022-06-1145-54NULL
2022-12-0925-340
2023-01-1035-44NULL
2023-04-2545-54NULL

Worked solution and explanation

What this really is

Strip the business costume off and this is a conditional-aggregation ratio with a scoping filter hiding underneath. The skill being probed: can you build error_and_crash_count over open_count, per day and per region, while restricting the universe to exactly the three event types that matter. Anyone can write the two CASE sums. The trap is the WHERE clause. Leave it out and every day a user merely clicked a button or made a purchase becomes its own output row with a NULL rate, and your 36 rows balloon toward 161.

`age_bucket` rides in from users and stands in for region; `event_type` and `event_timestamp` carry the metric and the day.
  • event_data: event_id INTEGER PK, event_type TEXT, user_id INTEGER FK, event_timestamp TEXT
  • users: user_id INTEGER PK, age_bucket TEXT
  • event_data.user_id references users.user_id

Building it

Step 1: Scope the event universe first

WHERE event_type IN ('open', 'error', 'crash') throws away clicks, purchases, signups, logins and the rest before anything is grouped. This is what stops a day where nobody opened or errored from leaking in as a phantom row with a NULL rate.

Step 2: Pull region from users

JOIN users ON event_data.user_id = users.user_id brings in age_bucket, which the report reads as region. The inner join also drops orphan events whose user_id has no match, which is what you want here.

Step 3: Express the ratio as two conditional sums

CAST(SUM(CASE WHEN event_type IN ('error','crash') THEN 1 ELSE 0 END) AS REAL) divided by SUM(CASE WHEN event_type = 'open' THEN 1 ELSE 0 END). The CAST forces real division, so 3 crashes over 10 opens reads 0.3 instead of integer 0. When a group has errors or crashes but zero opens, the divide-by-zero yields NULL, which is exactly what the preview shows for those rows.

Step 4: Collapse to one row per day and region

GROUP BY event_day, region produces one row per combination. event_day is the first ten characters of the timestamp string, substr(event_timestamp, 1, 10).

Per-day, per-region failure ratio
SELECT substr(ed.event_timestamp, 1, 10) AS event_day,
       u.age_bucket AS region,
       CAST(SUM(CASE WHEN ed.event_type IN ('error', 'crash') THEN 1 ELSE 0 END) AS REAL)
         / SUM(CASE WHEN ed.event_type = 'open' THEN 1 ELSE 0 END) AS error_rate
FROM event_data ed
JOIN users u ON ed.user_id = u.user_id
WHERE ed.event_type IN ('open', 'error', 'crash')
GROUP BY event_day, region

The WHERE filter and the opens-only denominator are the two load-bearing choices.

Without the WHERE filter
Every day/region with any activity becomes a row. Days of pure clicks and purchases have zero opens and zero errors, so the ratio is 0/0 = NULL, and the result swells to 161 rows of mostly noise.
With WHERE `event_type` IN (...)
Only days/regions that actually emitted an open, error, or crash survive. The result is the 36 rows the report is meant to show, with NULL reserved for groups that errored but never opened.

Common follow-up questions