Buyers Who Never Browsed
An easy SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- easy
- Seniority
- L3
- Asked in
- Fidelity
Problem
Some 2026 purchases were made by buyers who never once appear in page_views, and the fraud team's review needs every one of those transactions. List each with the buyer's username and the transaction total, smallest amount first.
They bought without ever loading a page.
Tables
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 |
transactions
Columns: transaction_id INTEGER, user_id INTEGER, total_amount REAL, transaction_date TEXT, product_id INTEGER, quantity INTEGER
| 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 |
page_views
Columns: view_id INTEGER, user_id INTEGER, viewed_at TEXT, page_url TEXT, referrer TEXT, dur_ms INTEGER, device TEXT
| view_id | page_url | user_id | referrer | dur_ms | device | viewed_at |
|---|---|---|---|---|---|---|
| 5053 | /products | 1555 | amazon.com | 637 | desktop | 2026-09-02 01:09:00 |
| 5106 | /checkout | 1652 | twitter.com | 774 | tablet | 2026-09-03 02:18:00 |
| 5159 | /profile | 1749 | github.com | 911 | Mobile | 2026-09-04 03:27:00 |
| 5212 | /search | 1846 | NULL | 1048 | DESKTOP | 2026-09-05 04:36:00 |
| 5265 | /about | 1943 | reddit.com | 1185 | mobile | 2026-09-06 05:45:00 |
| 5318 | /blog/intro | 1555 | NULL | NULL | desktop | 2026-09-07 06:54:00 |
| 5371 | /pricing | 1652 | bing.com | 1459 | tablet | 2026-09-08 07:03:00 |
| 5424 | /docs/api | 1749 | news.ycombinator.com | 1596 | Mobile | 2026-09-09 08:12:00 |
| 5477 | /settings | 1846 | NULL | 1733 | DESKTOP | 2026-09-10 09:21:00 |
| 5530 | /Home | 1943 | bing.com | 1870 | mobile | 2026-09-11 10:30:00 |
Expected output
| username | total_amount |
|---|---|
| arjun | 50.4 |
| aiden | 104.28 |
| beatrice | 158.16 |
| arjun | 252.45 |
| aiden | 306.33 |
Worked solution and explanation
What this is really asking
This is a set difference wearing a fraud-investigation costume: the year's purchases minus every buyer who shows up in page_views. Absence is the whole problem, and you cannot see absence by joining to the rows that exist. The candidate who inner-joins page_views returns precisely the browsers the review wanted gone, each one repeated about 62 times. The one who scopes page_views to the year through viewed_at quietly turns 'never browsed' into 'did not browse this year' and lets real browsers in. Any honest anti-join works; the trap is everything that is not one.
Break down the requirements
Step 1: Scope transactions to the target year
Filter the driving table to the target year: strftime('%Y', t.transaction_date) = '2026'. The year lives on transaction_date, so the page_views side stays completely unrestricted.
Step 2: Join to users for the username
Inner-join users on user_id to expose each buyer's username for the output. Every transaction references a real user, so this join neither adds nor drops rows.
Step 3: Exclude anyone with a page view
Apply the anti-join: the correlated NOT EXISTS keeps only buyers whose user_id never appears in page_views. One matching row is enough to drop the transaction, and nothing from page_views reaches the output.
Step 4: Order smallest purchase first
Sort by total_amount, smallest first. The only equal totals here are one buyer's repeat purchases at the same price, and those rows are identical, so a second sort key would change nothing.
Two anti-joins, one inverse
- Inner join to `page_views`
JOIN page_views pv ON pv.user_id = t.user_idkeeps only buyers who browsed: the inverse set, fanned out by every view. NoWHEREclause downstream can recover the buyers it already dropped.- Anti-join
NOT EXISTS(orLEFT JOINpluspv.user_id IS NULL) keeps the purchases with no partner. Both are correct;NOT EXISTSsays it directly and stops at the first match.
The solution
SELECT u.username, t.total_amount
FROM transactions t
JOIN users u ON t.user_id = u.user_id
WHERE strftime('%Y', t.transaction_date) = '2026'
AND NOT EXISTS (
SELECT 1 FROM page_views pv WHERE pv.user_id = t.user_id
)
ORDER BY t.total_amount;Common follow-up questions
- How would you find users who browsed but never bought? (Flips the driving table: `page_views` becomes the side you keep and `transactions` the side you negate.)
- What if you want transactions with no page view in the same calendar month? (Now the correlation must match both the user and the month: tests windowed vs unwindowed anti-joins.)
- Rewrite this using `LEFT JOIN` + `IS NULL`, and explain when you would prefer it. (Tests whether they know the anti-join forms and the fan-out trade-off between them.)