Every Touch Counts
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
- junior
- Asked in
- Copart
Problem
Find each shopper's 2026 touchpoint total on the vehicle auction marketplace, most active first, where every row in ad_impressions and every row in search_queries from that year counts as one touchpoint. Touchpoints logged without a user_id still count, reported together on a single row of their own.
A bid, a click, a search: each one is a vote. Total them honestly.
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
| user_id | touchpoint_count |
|---|---|
| NULL | 46 |
| 488 | 15 |
| 973 | 11 |
| 876 | 11 |
| 682 | 11 |
Worked solution and explanation
Why this problem exists in real interviews
This is the UNION vs UNION ALL question wearing a marketing hat. The real skill being probed: do you know that UNION quietly deduplicates rows while UNION ALL keeps every one? You are combining two activity streams and counting events per user. If you reach for UNION, two ad impressions from the same shopper collapse into a single row before the count ever runs, so a power user with 40 touchpoints reports the same number as someone who showed up once. Your 'most engaged' ranking becomes noise, and nothing in the output looks obviously wrong.
Break down the requirements
Step 1: Reduce each source to one row per event
From ad_impressions and from search_queries, select just user_id, each filtered to 2026 on its own time column. One row out per impression and per search is exactly what 'one touchpoint' means. Do not filter out a missing user_id: the statement wants those touchpoints kept as their own row.
Step 2: Stack the streams, keep every row
UNION ALL the two selects. This is the whole problem: UNION ALL preserves duplicates so the row count equals the event count. UNION would fold identical user_id rows together and destroy the metric before you can measure it.
Step 3: Count and order
Wrap the stacked stream in a subquery, GROUP BY user_id, COUNT(*) as touchpoint_count, then sort by touchpoint_count descending. GROUP BY puts every NULL user_id into one group, which is exactly the anonymous row the statement asks for.
The solution
SELECT user_id,
COUNT(*) AS touchpoint_count
FROM (
SELECT user_id FROM ad_impressions
WHERE strftime('%Y', impression_time) = '2026'
UNION ALL
SELECT user_id FROM search_queries
WHERE strftime('%Y', query_time) = '2026'
) activity
GROUP BY user_id
ORDER BY touchpoint_count DESC- UNION ALL (correct here)
- Appends both streams row for row. A user with 12 impressions and 8 searches contributes 20 rows, so COUNT(*) returns 20. No hidden work, no sort to dedup.
- UNION (wrong here)
- Runs an implicit DISTINCT across the whole stacked set. Selecting only
user_idmeans each user collapses to one row, so every COUNT(*) returns 1 and the ranking is meaningless. It also pays for a sort or hash you never wanted.
Common follow-up questions
- Now break the total into two columns: impressions and searches side by side per user. (Pushes toward conditional aggregation, tagging each branch with a source literal before the UNION ALL, then SUM(CASE ...).)
- What changes if you only want shoppers active in BOTH channels, not either? (Shifts from UNION ALL to an INTERSECT or a join/HAVING on distinct sources, testing set-operator fluency.)
- How would you keep this incremental as new impressions and searches stream in daily? (Probes partition pruning on the time columns and whether they would pre-aggregate per day rather than rescan the full history.)