When They Opened

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

Problem

Each row of push_notifs is one push notification, recording when it was sent and whether it was opened. Find how many notifications were opened in each month of the year, busiest month first.

Push after push goes out. Some months, people actually read them.

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

Expected output

First 5 of 9 rows
monthopened_count
79
49
127
66
81

Worked solution and explanation

What this really is

Take away the engagement story and this is a tally of a three-valued flag into month buckets you manufacture from a text timestamp. Anyone writes COUNT(*). What separates candidates is the grain and the filter. The bucket is the month of the year, pooled across every year the table spans. Only rows with opened = 1 are opens. If you group on the raw sent_at, you get one row per send. If you group by year and month, January splits five ways. If you count every row, each month absorbs its unopened and unflagged sends. Then comes the order: busiest first means sorting by opened_count, not by the calendar.

The trap

Building it

Step 1: Manufacture the month bucket

Derive the month with CAST(strftime('%m', sent_at) AS INTEGER). Casting here, once, means the SELECT, the GROUP BY and the ORDER BY all see a clean integer from 1 to 12.

Step 2: Keep only the opens

WHERE opened = 1 drops the zeros and the NULL flags before anything is counted. That way COUNT(*) counts confirmed opens, and a month with no opens never becomes a row.

Step 3: Tally and sort by volume

GROUP BY month collapses each calendar month into one row, COUNT(*) tallies its opens, and ORDER BY opened_count DESC puts the busiest month on top. Months 1, 4 and 7 tie at 9 opens. The question only asks for busiest first, so their order among themselves is free.

Per-month opened counts
SELECT CAST(strftime('%m', sent_at) AS INTEGER) AS month,
       COUNT(*) AS opened_count
FROM push_notifs
WHERE opened = 1
GROUP BY month
ORDER BY opened_count DESC
Filter, then count
WHERE opened = 1 followed by COUNT(*) states the intent where a reviewer can see it. NULLs and zeros are excluded by the predicate, and the count runs only over rows you have already vouched for.
Sum the flag
SUM(opened) over the whole table assumes the flag only ever holds 0 or 1, and it hides the filter inside the aggregate. It also keeps months 2, 10 and 11 as zero rows, which is three rows the answer does not contain.

Common follow-up questions