Email Census
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
Problem
Every row in users carries an account_status, and its email is NULL when no address is on file. Break the user base down by status and by whether an email exists, giving each combination's user count and its share of all users as a percentage rounded to one decimal place.
The reachability split.
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
| account_status | email_status | user_count | pct |
|---|---|---|---|
| active | has email | 46 | 23 |
| active | no email | 4 | 2 |
| inactive | has email | 47 | 23.5 |
| pending_verification | has email | 45 | 22.5 |
| suspended | has email | 46 | 23 |
Worked solution and explanation
What this really is
This is a percent-of-grand-total problem dressed up as a reachability census. Anyone can write the CASE that labels each account and the GROUP BY that counts it. What separates candidates is the denominator. Inside a grouped query each row sees only its own bucket, never the full user base, so the total has to come from outside the group. The second trap is quieter: write * 100 / and integer division truncates every share, and ROUND has nothing left to round.
Step 1: Label every account in place
CASE WHEN email IS NULL THEN 'no email' ELSE 'has email' END turns the missing-address property into a value you can group on, right in the SELECT. Only real absence is NULL. An empty string is still an address on file, and it belongs in 'has email'.
Step 2: Group on the status and the label expression
Put account_status and the same CASE expression in the GROUP BY. No intermediate result is needed: the engine evaluates the label once per row and buckets on it. Every status splits into its reachable and unreachable accounts, and COUNT(*) sizes each piece.
Step 3: Divide by the whole base, not the status
(SELECT COUNT(*) FROM users) is a scalar subquery that runs once and gives every row the same 5,000,000 denominator. Multiply by 100.0 first so the division stays fractional, then round to one decimal place.
SELECT
account_status,
CASE WHEN email IS NULL THEN 'no email' ELSE 'has email' END AS email_status,
COUNT(*) AS user_count,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM users), 1) AS pct
FROM users
GROUP BY
account_status,
CASE WHEN email IS NULL THEN 'no email' ELSE 'has email' END- Integer math
COUNT(*) * 100 / (SELECT COUNT(*) FROM users)keeps only the whole number in engines that divide integers exactly. A 2.4% segment reports 2, a 0.3% segment reports 0, andROUND(..., 1)is rounding a value that was already truncated.- Fractional math
COUNT(*) * 100.0 / ...promotes the arithmetic to decimal before dividing, so 2.4 survives andROUNDdoes real work.
Common follow-up questions
- What changes if blank `email` values should also count as unreachable? (Tests folding `NULLIF(TRIM(email), '')` into the null check.)
- How would you report each label's share within its own `account_status` instead? (Tests switching to a partitioned window total.)
- Can you get the grand total without reading `users` twice? (Tests `SUM(COUNT(*)) OVER ()` over the aggregated rows.)