Click-Through by Campaign

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
Airbnb

Problem

A campaign's clicked and not-clicked shares are percentages of that campaign's own impressions, kept to two decimal places. List every campaign in ad_impressions alphabetically with its total impressions and both shares.

Which campaigns actually got the tap.

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 8 rows
ad_campaigntotal_impressionsclicked_pctnot_clicked_pct
BRAND_AWARENESS_Q11717.6582.35
FLASH_SALE_48H1526.6773.33
HOLIDAY_PROMO162575
LOYALTY_PROGRAM8484.5215.48
SUMMER_SALE_20242133.3366.67

Worked solution and explanation

This is a one-pass pivot of a 0/1 flag dressed up as an engagement report. The pivot is not what separates candidates: the division is. clicked is an integer, a SUM over a CASE is an integer, and COUNT(*) is an integer, and integer over integer floors. Write SUM(...) * 100 / COUNT(*) and the 3 clicks out of 17 impressions for 'BRAND_AWARENESS_Q1' come back as 17, not 17.65. Every share in the report loses its decimals, and the two shares of a campaign stop adding to 100.

Walking to the query

Step 1: Read only ad_impressions

push_notifs has a campaign column of its own, and that is the bait. Its values ('loyalty_2024', 'flash_sale') are not the ad campaigns. Joining it fans every impression out across notification rows and inflates total_impressions. Everything this report needs lives on the impression row.

Step 2: One row per campaign, one denominator

GROUP BY ad_campaign collapses the log to eight rows, and COUNT(*) is the denominator for both shares. Use COUNT(*), not COUNT(revenue): revenue is NULL on every unclicked impression, so COUNT(revenue) quietly counts clicks and both shares come out wrong.

Step 3: Pivot the flag into two counts

SUM(CASE WHEN clicked = 1 THEN 1 ELSE 0 END) counts the clicks inside the same group, and the mirrored clicked = 0 branch counts the misses. Both counts sit side by side on the campaign row, and there is no second pass or self-join.

Step 4: Make the numerator real before you divide

CAST(... AS REAL) turns the click count into a real number, so * 100 / COUNT(*) keeps its fraction. ROUND(..., 2) then trims it to the two decimals the report shows. Converting one operand before the division is enough; converting after it is too late.

Clicked and not-clicked shares per campaign
SELECT
    ad_campaign,
    COUNT(*) AS total_impressions,
    ROUND(CAST(SUM(CASE WHEN clicked = 1 THEN 1 ELSE 0 END) AS REAL) * 100 / COUNT(*), 2) AS clicked_pct,
    ROUND(CAST(SUM(CASE WHEN clicked = 0 THEN 1 ELSE 0 END) AS REAL) * 100 / COUNT(*), 2) AS not_clicked_pct
FROM ad_impressions
GROUP BY ad_campaign
ORDER BY ad_campaign

One scan of ad_impressions: both counts are pivoted in the group, made real, then divided.

Split by the flag
GROUP BY ad_campaign, clicked with COUNT(*) gives up to two rows per campaign, holding raw counts and no shares. You would still need a second step to bring the campaign total back onto each row.
Pivot the flag
GROUP BY ad_campaign with two conditional sums puts both counts and their shared COUNT(*) on one row, so each share is a single expression over the same group.

Common follow-up questions