Missing Email for Non-Active Users
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
- City and County of San Francisco
Problem
Which non-active accounts in users are too incomplete for a win-back campaign, with no email to send to (never filled in or saved blank) or no age bracket to segment by? The lifecycle marketer building the list wants each such user with the year they signed up, whatever their specific non-active status.
No email on file. No recent activity. Something smells off.
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 |
Expected output
| user_id | signup_year |
|---|---|
| 585 | 2024 |
| 1070 | 2026 |
| 1264 | 2025 |
| 2622 | 2024 |
| 11352 | 2023 |
Worked solution and explanation
Three ways to be incomplete
This is a 'what counts as missing' question dressed up as a marketing list. Pulling a year off a date and excluding active accounts is the easy part. What separates candidates is that an incomplete profile hides three ways in users: an email that is NULL, an email saved as '', and an age_bucket that is NULL. Miss one and the list still looks plausible; drop the parentheses and the status filter only guards the last check, so active users ride along. Either way the campaign targets the wrong people.
Building the list
Step 1: Catch both storage forms of a missing email
email = '' never matches a NULL row, because that comparison is never true and a WHERE keeps only true rows. email IS NULL never matches a blank. You need both, or a form that folds them together like COALESCE(email, '') = ''.
Step 2: Add the missing age bracket
An account with an address but no age_bucket still cannot be segmented, so age_bucket IS NULL is a third alternative in the same condition.
Step 3: Group, then exclude active accounts
Wrap the three alternatives in one set of parentheses, then add account_status <> 'active'. That keeps 'inactive', 'suspended' and 'pending_verification' alike, and the negation applies to every alternative.
Step 4: Extract the year from signup_date
signup_date is text, so STRFTIME('%Y', signup_date) returns the four-digit year the marketer segments the list by.
SELECT
user_id,
STRFTIME('%Y', signup_date) AS signup_year
FROM users
WHERE (email IS NULL OR email = '' OR age_bucket IS NULL)
AND account_status <> 'active'- No parentheses
email IS NULL OR email = '' OR age_bucket IS NULL AND account_status <> 'active'reads as three separate alternatives, with the status test glued to the last. Active users with no email come back.- Grouped condition
(email IS NULL OR email = '' OR age_bucket IS NULL) AND account_status <> 'active'applies the status test to every alternative. Only lapsed accounts remain.
Common follow-up questions
- What if some `email` values are just whitespace, like ' '? (Tests extending the missing-value definition with `TRIM(email) = ''`.)
- How would you report how many of these users signed up in each year? (Tests moving from a row list to a per-year count keyed on the extracted year.)
- How would you stop new blank emails from being written at all? (Tests pushing the fix upstream: normalize '' to `NULL` on write, or a validation rule.)