Impressions by Search Keyword
A hard SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- hard
- Seniority
- L3
- Asked in
- Meta
Problem
Someone who ran several searches in search_queries is still one member of the audience, so each impression they received in ad_impressions counts once, not once per search. Leaving out the time-boxed sale campaigns (any ad_campaign with 'SALE' in its name), ad sales wants every other campaign's impressions that reached a searcher, from most impressions to least.
Campaign performance, keyword by keyword.
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 |
search_queries
Columns: query_id INTEGER, user_id INTEGER, search_term TEXT, results_count INTEGER, clicked_result INTEGER, query_time TEXT
| query_id | user_id | search_term | results_count | clicked_result | query_time |
|---|---|---|---|---|---|
| 2071 | 100 | laptop deals | 17 | 2 | 2026-02-02 01:11:00 |
| 2142 | 197 | phone case iphone 15 | 34 | 3 | 2026-03-03 02:22:00 |
| 2213 | 294 | usb-c cable 6ft | 51 | NULL | 2026-04-04 03:33:00 |
| 2284 | NULL | gaming mouse | 68 | 5 | 2026-05-05 04:44:00 |
| 2355 | 488 | mechanical keyboard | 85 | 6 | 2026-06-06 05:55:00 |
| 2426 | 585 | 4k monitor | 102 | NULL | 2026-07-07 06:06:00 |
| 2497 | 682 | standing desk | 119 | 8 | 2026-08-08 07:17:00 |
| 2568 | NULL | webcam 1080p | 136 | 9 | 2026-09-09 08:28:00 |
| 2639 | 876 | bluetooth speaker | 153 | NULL | 2026-01-10 09:39:00 |
| 2710 | 973 | smart watch | 170 | 1 | 2026-02-11 10:50:00 |
Expected output
| ad_campaign | impression_count |
|---|---|
| LOYALTY_PROGRAM | 58 |
| BRAND_AWARENESS_Q1 | 17 |
| RETARGETING_CART | 16 |
| NEW_USER_ACQU | 15 |
| PRODUCT_LAUNCH_X | 7 |
Worked solution and explanation
What this really is
This is a fan-out join dressed up as a campaign-reach report. Anyone can join search_queries to ad_impressions on user_id and count by campaign. What separates candidates is noticing that search_queries holds one row per search, not one per person. Each search row re-matches every impression that user ever saw, so a user with two searches counts every ad twice. Miss it and every campaign's reach is inflated, quietly, with numbers that still look plausible.
Build it in order
Step 1: Collapse searches to searchers
SELECT DISTINCT user_id FROM search_queries turns searches into searchers. The statement says a person is one audience member, so this is the grain the join needs. Doing it first keeps the join input small and the count honest.
Step 2: Join the searchers to their impressions
Inner-join searchers to ad_impressions on user_id. Impressions with a null user_id match nobody and drop out; a campaign that reached no searcher never appears.
Step 3: Drop the sale campaigns
ad_campaign NOT LIKE '%SALE%' removes 'FLASH_SALE_48H' and 'SUMMER_SALE_2024'. The word sits in the middle of one name and the end of the other, so both wildcards matter.
Step 4: Count per campaign, biggest first
GROUP BY ad_campaign, count the rows as impression_count, and sort by it descending. That is the whole order the statement asks for; campaigns tied on the count may come back either way.
SELECT ai.ad_campaign, COUNT(*) AS impression_count
FROM (
SELECT DISTINCT user_id
FROM search_queries
) searchers
INNER JOIN ad_impressions ai ON ai.user_id = searchers.user_id
WHERE ai.ad_campaign NOT LIKE '%SALE%'
GROUP BY ai.ad_campaign
ORDER BY impression_count DESC;The derived table searchers holds one row per person who searched.
SELECT ai.ad_campaign, COUNT(*) AS impression_count
FROM search_queries sq
INNER JOIN ad_impressions ai ON ai.user_id = sq.user_id
WHERE ai.ad_campaign NOT LIKE '%SALE%'
GROUP BY ai.ad_campaign
ORDER BY impression_count DESC;Every search row re-joins all of that user's impressions.
- Dedupe after the join
COUNT(DISTINCT ai.impression_id)over the raw join lands on the same numbers, but the engine first materializes every fanned-out row and only then throws the duplicates away.- Dedupe before the join
DISTINCT user_idremoves the repeats on the small side, so the join never produces them and a plainCOUNT(*)is correct.
Common follow-up questions
- If only impressions within a day after a search should count, what happens to the `DISTINCT user_id` step? (Tests attribution-window thinking: each search has its own `query_time`, so collapsing to searchers first is no longer valid.)
- If sales wanted the same report broken down by `search_term`, what would you dedupe on, and what would `search_term` need first? (Tests whether the dedupe grain follows the report grain.)
- How would you report the number of unique searchers each campaign reached instead of impressions? (Tests reach versus frequency.)