First Interaction Credit
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
- L5
- Asked in
- Netflix
Problem
Pull each converted user's user_id with the ad_campaign and impression_time of the very first impression they were ever served in ad_impressions, clicked or not, since the analyst building the first-touch attribution credits every conversion there. A user counts as converted as soon as they show up in transactions.
Attribute transactions to earliest touchpoint
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 |
transactions
Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT
| transaction_id | user_id | product_id | quantity | total_amount | transaction_date |
|---|---|---|---|---|---|
| 1067 | 197 | 1001 | 2 | 23.46 | 2026-02-02 |
| 1134 | 294 | 1050 | 3 | 36.93 | 2026-03-03 |
| 1201 | 391 | 1099 | 4 | 50.4 | 2026-04-04 |
| 1268 | 488 | 1148 | 5 | 63.87 | 2026-05-05 |
| 1335 | 585 | 1197 | 0 | 0 | 2026-06-06 |
| 1402 | 682 | 1246 | 2 | 90.81 | 2026-07-07 |
| 1469 | 779 | 1295 | 3 | 104.28 | 2026-08-08 |
| 1536 | 876 | 1344 | 4 | 117.75 | 2026-09-09 |
| 1603 | 973 | 1393 | 5 | -45 | 2026-01-10 |
| 1670 | 1070 | 1442 | 1 | 144.69 | 2026-02-11 |
Expected output
| user_id | ad_campaign | impression_time |
|---|---|---|
| 100 | BRAND_AWARENESS_Q1 | 2022-01-02 01:07:00 |
| 197 | PRODUCT_LAUNCH_X | 2023-02-03 02:14:00 |
| 294 | NEW_USER_ACQU | 2024-01-14 13:31:00 |
| 391 | LOYALTY_PROGRAM | 2025-02-15 14:38:00 |
| 488 | NEW_USER_ACQU | 2026-01-18 21:15:00 |
Worked solution and explanation
What this is really asking
This is a per-user argmin wearing an attribution badge. Finding each converted user's earliest impression_time is the easy half. The skill is returning the ad_campaign from that same row. GROUP BY user_id with MIN(impression_time) gets the timestamp right and then has to pick a campaign some other way, and MIN(ad_campaign) picks the alphabetically first campaign, not the first-touch one. Fall for that and the timestamps look perfect while each conversion is credited to a campaign the user may have seen years later.
Break down the requirements
Step 1: Restrict to converters
A converter is any user_id present in transactions, and each one has 12 to 14 purchase rows. Test membership with EXISTS instead of joining: a join repeats every impression once per purchase before you have picked anything. The impressions with a NULL user_id match no transaction and drop out here too.
Step 2: Pick the earliest impression row
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY impression_time) numbers each converter's impressions oldest first, and rn = 1 is the first touch. The filter sits outside the CTE because a window value cannot be referenced in the WHERE of the query that computes it. There is no predicate on clicked: a view counts.
Step 3: Return the trio
Project user_id, ad_campaign and impression_time from the surviving row. That leaves one row per converted user, ten on this data, with no aggregation left to do and no order to promise.
The solution
WITH ranked AS (
SELECT
i.user_id,
i.ad_campaign,
i.impression_time,
ROW_NUMBER() OVER (
PARTITION BY i.user_id
ORDER BY i.impression_time
) AS rn
FROM ad_impressions i
WHERE EXISTS (
SELECT 1 FROM transactions t WHERE t.user_id = i.user_id
)
)
SELECT user_id, ad_campaign, impression_time
FROM ranked
WHERE rn = 1
;- A minimum per column
GROUP BY user_idwithMIN(impression_time)andMIN(ad_campaign)computes two independent minimums. The timestamp is the first touch; the campaign is whichever name sorts first among everything the user saw, which matches only by luck.- Pick the whole row
ROW_NUMBERorders the rows themselves, sorn = 1returns the timestamp and the campaign of one physical impression. A self-join back toMIN(impression_time)also works, but it readsad_impressionstwice.
Common follow-up questions
Common follow-up questions
- How would you switch this to a last-touch model? (Flip the `ORDER BY` inside the window to `DESC`; the rest is identical.)
- What if two impressions share an `impression_time` for the same user? (`ROW_NUMBER` then picks one arbitrarily. Add `impression_id` as a tiebreaker, or use `RANK` and decide how to split credit.)
- How do you handle users who converted before their first impression? (Add `t.transaction_date >= i.impression_time` inside the `EXISTS`, or you credit campaigns for purchases they could not have caused.)