Left on Read

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
Amazon

Problem

Our push notification system records an opened flag on every notification along with the campaign that sent it. Across every campaign that has sent notifications, find the ones whose opens outnumber those of the average campaign.

Every campaign fights for the tap. Find the ones the field is leaving behind.

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 5 rows
campaignopen_count
flash_sale8
onboard_v28
promo_summer7
reengagement6
winback_feb6

Worked solution and explanation

What this really is


Break down the requirements

Step 1: Tally opens with SUM, not a pre-filter

An open is opened = 1; a 0 or a NULL is not. Count opens with SUM(opened): it adds up the set flags and ignores the zeros and the blanks, so you get each campaign's open tally without ever writing WHERE opened = 1. That matters, because that filter would erase the campaigns that earned no opens, and those campaigns still belong in the field you are measuring against.

Step 2: Drop the phantom no-campaign group

Filter campaign IS NOT NULL before you group, in the standings and again in the average. A notification with no campaign is not a campaign, and its rows are the single largest bucket in the table. Leave them in and they both win the standings outright with 21 opens and inflate the opens the average is built from, poisoning the very bar you are comparing everyone against.

Step 3: Build the yardstick at the campaign grain

The average campaign is total opens divided by the number of campaigns: SUM(opened) * 1.0 / COUNT(DISTINCT campaign). The trick is the denominator. COUNT(DISTINCT campaign) counts every campaign that sent anything, including the ones that earned zero opens, so those quiet campaigns still weigh in the average even though they add nothing to the numerator. Keep the campaigns whose tally clears that bar.


The solution

Keep the campaigns whose opens clear the average campaign
SELECT campaign, SUM(opened) AS open_count
FROM push_notifs
WHERE campaign IS NOT NULL
GROUP BY campaign
HAVING SUM(opened) > (
    SELECT SUM(opened) * 1.0 / COUNT(DISTINCT campaign)
    FROM push_notifs
    WHERE campaign IS NOT NULL
)
AVG over rows (wrong)
AVG(opened) across every notification yields the per-notification open rate, about 0.35 here. Any campaign that logged even one open clears it, so the filter stops separating strong campaigns from weak ones and the grain is simply wrong.
average over campaigns (right)
SUM(opened) * 1.0 / COUNT(DISTINCT campaign) yields the opens the typical campaign earned, 5 on this data. Keeping the campaigns whose tally clears that bar surfaces the genuine over-performers.

Common follow-up questions