Loyalty's Double Tap

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

Count the impressions each ad_campaign in ad_impressions served to customers who received a loyalty push, broken down by the platform that push went out on, as impression_count. For the growth team, a loyalty push is any push_notifs row whose campaign has 'loyalty' in its name, and the log's platform spellings such as 'Android' and 'android' are one platform, not two.

When a nudge and a banner team up.

Tables

ad_impressions

Columns: impression_id INTEGER, user_id INTEGER, ad_campaign TEXT, impression_time TEXT, clicked INTEGER, revenue REAL

ad_impressions: 10 of 200 rows
impression_iduser_idad_campaignimpression_timeclickedrevenue
253100BRAND_AWARENESS_Q12026-02-02 01:07:000NULL
306197PRODUCT_LAUNCH_X2026-03-03 02:14:000NULL
359294RETARGETING_CART2026-04-04 03:21:000NULL
412391HOLIDAY_PROMO2026-05-05 04:28:000NULL
465488NEW_USER_ACQU2026-06-06 05:35:0010.2
518NULLLOYALTY_PROGRAM2026-07-07 06:42:000NULL
571682FLASH_SALE_48H2026-08-08 07:49:000NULL
624779SUMMER_SALE_20242026-09-09 08:56:000NULL
677876BRAND_AWARENESS_Q12026-01-10 09:03:000NULL
730973PRODUCT_LAUNCH_X2026-02-11 10:10:0010.35

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 6 rows
ad_campaignplatformimpression_count
BRAND_AWARENESS_Q1web3
FLASH_SALE_48Hweb4
LOYALTY_PROGRAMandroid6
NEW_USER_ACQUweb4
SUMMER_SALE_2024ios1

Worked solution and explanation

What this is really asking

Underneath the growth framing this is a membership-filtered overlap count with a dirty dimension: for each display-ad campaign and each push platform, how many impressions landed on customers a loyalty push targeted. Membership and platform live only in push_notifs, so the inner join on user_id carries them in. Anyone writes the join. Two things separate candidates. The log spells the same platform 'Android' and 'android', so grouping on the raw platform hands back one platform as two half-sized rows. And once the spellings are folded, a customer pushed under both spellings repeats every impression in that group, so a bare COUNT(*) inflates it. Both mistakes run clean.


Break down the requirements

Step 1: Define a loyalty push

A row in push_notifs qualifies when campaign LIKE '%loyalty%'. Delivery outcome and the opened flag are not part of the definition: being targeted is the membership.

Step 2: Keep only impressions for those customers

Inner join ad_impressions to the loyalty pushes on user_id. Impressions with a null user_id, and customers no loyalty push targeted, fall out of the join, which is exactly the population you want gone.

Step 3: Fold platform before it becomes a key

Use LOWER(pn.platform) in both the SELECT and the GROUP BY. If only one of them folds, the engine either rejects the query or still splits the groups; the key you group on and the key you show have to be the same expression.

Step 4: Count each impression once

Use COUNT(DISTINCT ai.impression_id). The join emits one row per impression per matching push, so the impression id is the only honest unit to count.


The solution

Inner join, folded platform, count each impression once
SELECT ai.ad_campaign,
       LOWER(pn.platform) AS platform,
       COUNT(DISTINCT ai.impression_id) AS impression_count
FROM ad_impressions ai
INNER JOIN push_notifs pn ON ai.user_id = pn.user_id
WHERE pn.campaign LIKE '%loyalty%'
GROUP BY ai.ad_campaign, LOWER(pn.platform)
Platform grouped as written
GROUP BY ad_campaign, platform treats 'Android' and 'android' as two platforms. Each half looks small, and anyone summing by platform downstream has to clean it again.
Platform folded first
GROUP BY ad_campaign, LOWER(platform) puts both spellings in one group, so each platform reads as the single number it is.

Common follow-up questions