The Widest Net
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
- Travelport
Problem
Find each campaign's true reach from ad_impressions: how many different registered users clicked at least one of its ads, counting a person once however many times they clicked. The analyst building the campaign scorecard reads it widest-reaching first.
Not the clicks. The crowd: how many people each campaign actually pulled in.
Tables
ad_impressions
Columns: impression_id INTEGER, user_id INTEGER, ad_campaign TEXT, impression_time TEXT, clicked INTEGER, revenue REAL
| impression_id | user_id | ad_campaign | impression_time | clicked | revenue |
|---|---|---|---|---|---|
| 253 | 100 | BRAND_AWARENESS_Q1 | 2026-02-02 01:07:00 | 0 | NULL |
| 306 | 197 | PRODUCT_LAUNCH_X | 2026-03-03 02:14:00 | 0 | NULL |
| 359 | 294 | RETARGETING_CART | 2026-04-04 03:21:00 | 0 | NULL |
| 412 | 391 | HOLIDAY_PROMO | 2026-05-05 04:28:00 | 0 | NULL |
| 465 | 488 | NEW_USER_ACQU | 2026-06-06 05:35:00 | 1 | 0.2 |
| 518 | NULL | LOYALTY_PROGRAM | 2026-07-07 06:42:00 | 0 | NULL |
| 571 | 682 | FLASH_SALE_48H | 2026-08-08 07:49:00 | 0 | NULL |
| 624 | 779 | SUMMER_SALE_2024 | 2026-09-09 08:56:00 | 0 | NULL |
| 677 | 876 | BRAND_AWARENESS_Q1 | 2026-01-10 09:03:00 | 0 | NULL |
| 730 | 973 | PRODUCT_LAUNCH_X | 2026-02-11 10:10:00 | 1 | 0.35 |
users
Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT
| user_id | username | signup_date | account_status | age_bucket | |
|---|---|---|---|---|---|
| 100 | alice | alice@example.com | 2025-02-02 | inactive | 25-34 |
| 197 | aaron42 | aaron42@example.com | 2026-03-03 | suspended | 35-44 |
| 294 | amelia | amelia@example.com | 2024-04-04 | pending_verification | 45-54 |
| 391 | arjun | arjun@example.com | 2025-05-05 | active | 55-64 |
| 488 | ava99 | ava99@example.com | 2026-06-06 | inactive | 65+ |
| 585 | andrew_k | andrew_k@example.com | 2024-07-07 | suspended | NULL |
| 682 | anika | anika@example.com | 2025-08-08 16:34:00 | pending_verification | 18-24 |
| 779 | aiden | aiden@example.com | 2026-09-09 14:53:00 | active | 25-34 |
| 876 | aria_b | aria_b@example.com | 2024-10-10 | inactive | 35-44 |
| 973 | brian | brian@example.com | 2025-11-11 | suspended | 45-54 |
Expected output
| ad_campaign | users_reached |
|---|---|
| LOYALTY_PROGRAM | 13 |
| SUMMER_SALE_2024 | 6 |
| RETARGETING_CART | 1 |
| PRODUCT_LAUNCH_X | 1 |
| NEW_USER_ACQU | 1 |
Worked solution and explanation
What this really asks
This is a count of different people dressed up as a campaign scorecard. Anyone can group ad_impressions by ad_campaign. What separates candidates is where the click condition and the per-person collapse land. Reach is how many different users clicked, so two things have to happen to user_id inside the same count: unclicked impressions drop out, and a person who clicked ten times counts once. Count click rows and LOYALTY_PROGRAM reports 71 instead of 13. Count different users without the click condition and you have measured who saw the ads, not who acted on them.
- Counting click events
COUNT(*)orSUM(clicked)over clicked rows counts events. On this dataLOYALTY_PROGRAMhas 71 clicked impressions because the same monthly clickers come back year after year, so the campaign looks more than five times wider than it is.- Counting different clickers
COUNT(DISTINCT ...)over the clickinguser_idcollapses each person's repeat clicks. Those 71 rows come from 13 different users (one click carries no user at all), so reach is 13.
Building it
Step 1: Make the click condition part of the count
Group ad_impressions by ad_campaign and count CASE WHEN clicked = 1 THEN user_id END. A clicked row hands its user_id to the count; an unclicked row hands it NULL, which the count skips. Drop the CASE and the same query counts every viewer: exposure, not reach.
Step 2: Collapse repeat clickers with DISTINCT
COUNT(DISTINCT ...) keeps one entry per person per campaign. SUMMER_SALE_2024 has 7 clicked impressions from 6 people, and the gap is far wider wherever a regular keeps clicking. Anonymous clicks with a NULL user_id fall out for free, which is what 'registered users' asks for.
Step 3: Put the widest reach first
Sort by users_reached descending. Six campaigns tie at one clicker each. Nothing in the ask orders them against each other, so any order among them is correct.
The solution
SELECT ai.ad_campaign,
COUNT(DISTINCT CASE WHEN ai.clicked = 1 THEN ai.user_id END) AS users_reached
FROM ad_impressions ai
GROUP BY ai.ad_campaign
ORDER BY users_reached DESCCommon follow-up questions
- The team now wants reach limited to the last 30 days. Where does that date filter go, and can it live in WHERE without dropping zero-click campaigns? (Tests when a WHERE filter is safe (it narrows the fact scan) versus when it silently removes whole groups.)
- How would you also express each campaign's reach as a percentage of the total registered user base, in the same query? (Tests bringing in a global denominator via a scalar subquery over `users` without fanning the big table.)
- If one person clicked the same campaign from two device rows carrying different `user_ids`, how would you avoid counting them twice? (Tests identity resolution and what 'different user' really means when the keys are imperfect.)