The Notification Lifecycle

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

Problem

Our push notification system records a delivery status for every message, but that status is entered inconsistently, so the same outcome shows up in different letter cases. For each registered user, count how many of their notifications were delivered, how many were opened, and how many failed, where an open is captured by its own flag rather than the status text.

Sent, opened, ignored. What happened after the alert went out?

Tables

push_notifs

Columns: notif_id INTEGER, user_id INTEGER, title TEXT, platform TEXT, status TEXT, sent_at TEXT, opened INTEGER, campaign TEXT

push_notifs: 10 of 200 rows
notif_iduser_idtitleplatformstatussent_atopenedcampaign
1053100New message from teamandroidpending2026-02-02 01:07:000winback_feb
1106197Weekly digest readywebfailed2026-03-03 02:14:000promo_summer
1159294Price drop alertiOSbounced2026-04-04 03:21:001onboard_v2
1212391Security alertAndroidDelivered2026-05-05 04:28:000NULL
1265488Payment receivedWEBFAILED2026-06-06 05:35:00NULLloyalty_2024
1318585Complete your profilebasicdelivered2026-07-07 06:42:001NULL
1371682New feature availablepremiumpending2026-08-08 07:49:000reengagement
1424779Subscription renewaliosfailed2026-09-09 08:56:000NULL
1477876Flash sale: 50% offandroidbounced2026-01-10 09:03:001flash_sale
1530973Your order shipped!webDelivered2026-02-11 10:10:00NULLretention_q1

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 100 rows
user_iddelivered_countopened_countfailed_count
100000
197002
294020
391200
585220

Worked solution and explanation

What this is really about

Under the notification-dashboard costume, this is a conditional pivot: collapse many notification rows per user into one row with three counts. Everyone can write the CASE sums. The two things that actually separate candidates are hiding in the data: the status text is logged in mixed casing, and 'opened' is not a status at all. Match status case-sensitively and you silently undercount every 'Delivered' and 'FAILED'; treat opened as a status value and the column comes back all zeros because no such status exists.


The two traps

Naive (both traps)
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) and SUM(CASE WHEN status = 'opened' THEN 1 ELSE 0 END). Delivered undercounts on mixed casing; opened is always 0.
Correct
SUM(CASE WHEN LOWER(status) = 'delivered' THEN 1 ELSE 0 END) and SUM(CASE WHEN opened = 1 THEN 1 ELSE 0 END). Casing normalized, opens read from the real signal.

Building it

Step 1: Restrict to registered users

Join push_notifs to users on user_id. The inner join is the 'registered users only' filter: any notification whose user_id has no matching row is dropped, and users with no notifications never appear.

Step 2: One row per user

GROUP BY user_id collapses each user's many notification rows into a single output row. The three CASE sums are evaluated within each group.

Step 3: Pivot the three metrics

delivered_count and failed_count come from LOWER(status) so casing cannot split a count; opened_count comes from opened = 1 so a null flag counts as not opened. Three independent conditional sums, one pass over the group.

The solution

Conditional pivot per user
SELECT
    pn.user_id,
    SUM(CASE WHEN LOWER(pn.status) = 'delivered' THEN 1 ELSE 0 END) AS delivered_count,
    SUM(CASE WHEN pn.opened = 1 THEN 1 ELSE 0 END) AS opened_count,
    SUM(CASE WHEN LOWER(pn.status) = 'failed' THEN 1 ELSE 0 END) AS failed_count
FROM push_notifs pn
JOIN users u ON pn.user_id = u.user_id
GROUP BY pn.user_id

Common follow-up questions