Searches by Users With Email
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
- ESPN
Problem
The search quality team is analyzing result effectiveness but only for identifiable users. Pull all search query details for users who have an email address on file, limited to queries that returned at least one result.
One user's search behavior.
Tables
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 |
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
| 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 |
| 2355 | 488 | mechanical keyboard | 85 | 6 | 2026-06-06 05:55:00 |
| 2426 | 585 | 4k monitor | 102 | NULL | 2026-07-07 06:06:00 |
Worked solution and explanation
What this really is
This is a two-part identity filter dressed up as a search-quality report. The join to users feels like it already means 'users who have an email on file', and that feeling is the trap. An inner join matches on user_id, not on email, so a user row whose email is NULL joins just as happily as one with an address. Anyone can write the join. What separates candidates is the explicit u.email IS NOT NULL that the join cannot express, plus the results_count > 0 gate. Skip the email test and every search from an emailless user leaks into a report that was meant to hold only identifiable people.
Step 1: Join search_queries to users on user_id
A search row carries no email, so the join is the only way to see one. As a bonus, an inner join drops the anonymous searches whose user_id is NULL, because NULL never equals anything. That is the business rule (no person, not identifiable), so reaching for a LEFT JOIN here would be wrong.
Step 2: Test email explicitly
A matched user row is not proof of an address. On this data a slice of users have a NULL email, and they own searches. Only u.email IS NOT NULL removes them, and no join condition on user_id can do that job.
Step 3: Keep searches that returned something
sq.results_count > 0 keeps searches with at least one result. It also quietly drops rows where results_count is NULL, since a comparison with NULL is never true. That is the right reading: an unknown count is not evidence that results came back.
Step 4: Return the search columns only
The ask is search query details, so project the six search_queries columns with the sq. prefix. Leaving email or username out keeps the output in the shape the preview shows and avoids an ambiguous user_id.
SELECT sq.query_id, sq.user_id, sq.search_term, sq.results_count, sq.clicked_result, sq.query_time
FROM search_queries sq
JOIN users u ON sq.user_id = u.user_id
WHERE u.email IS NOT NULL AND sq.results_count > 0- Join alone
JOIN users u ON sq.user_id = u.user_idkeeps every search whose user exists, including users with a NULLemail. The row count looks plausible, which is why nobody notices.- Join plus the email test
- Adding
u.email IS NOT NULLremoves exactly the emailless users. The result shrinks to people the team can actually identify, which is the whole point of the report.
Common follow-up questions
- What changes if a missing email is sometimes stored as an empty string instead of NULL? (Tests whether the candidate knows `IS NOT NULL` does not catch '' and would add `NULLIF` or a length check.)
- If `users` could hold two rows for one `user_id`, what would happen to this output? (Tests awareness of join fan-out duplicating search rows.)
- How would you turn this into a count of qualifying searches per user? (Tests moving from a row filter to an aggregate without losing the NULL handling.)