Content Viewer Penetration
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
- Rockerbox
Problem
What percentage of our registered users have at least one view in content_views? We measure adoption against everyone in users, so accounts that have never watched anything still count toward the total.
What share of the user base has viewed at least one piece of content
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 |
content_views
Columns: view_id INTEGER, content_id INTEGER, user_id INTEGER, device_id INTEGER, viewed_at TEXT, watch_seconds INTEGER
| view_id | content_id | user_id | device_id | viewed_at | watch_seconds |
|---|---|---|---|---|---|
| 7053 | 372 | 1361 | 404 | 2026-02-02 01:07:00 | 41 |
| 7106 | 501 | 2622 | 607 | 2026-03-03 02:14:00 | 72 |
| 7159 | 630 | 3883 | 810 | 2026-04-04 03:21:00 | 103 |
| 7212 | 759 | 5144 | 1013 | 2026-05-05 04:28:00 | 134 |
| 7265 | 888 | 6405 | 1216 | 2026-06-06 05:35:00 | 165 |
| 7318 | 1017 | 7666 | 1419 | 2026-07-07 06:42:00 | 196 |
| 7371 | 1146 | NULL | 1622 | 2026-08-08 07:49:00 | 227 |
| 7424 | 1275 | 488 | 1825 | 2026-09-09 08:56:00 | 258 |
| 7477 | 1404 | 1749 | NULL | 2026-01-10 09:03:00 | 289 |
| 7530 | 1533 | 3010 | 2231 | 2026-02-11 10:10:00 | 320 |
Expected output
| viewer_pct |
|---|
| 43 |
Worked solution and explanation
This is a set-membership ratio dressed up as a growth metric: of the people in users, how many appear at least once in content_views? Everyone writes a join and a division. What separates candidates is which table the join is anchored on and what gets counted. Anchor on content_views, or inner-join it, and the 114 users who never watched anything drop out of the base, so adoption reads 100%. Count joined rows instead of people and every viewer counts twice, which lands at 60.14%. The real answer, 86 viewers out of 200 users, is 43.0%.
Building it
Step 1: Anchor the base on users
Start from users and LEFT JOIN content_views on user_id. Every registered user survives the join, and the 114 who never viewed come through as one row with a NULL cv.user_id. The 28 view rows with no user_id match nobody, so they cannot inflate anything.
Step 2: Count each viewer once
The join repeats a viewer once per view. On this data every viewer has two rows, 172 in all. COUNT(DISTINCT cv.user_id) collapses them to 86 people, and because COUNT skips NULL, the never-viewers add nothing to the numerator.
Step 3: Count the base as people too
COUNT(*) after the join is 286: 172 view rows plus 114 never-viewer rows. The base is people, so COUNT(DISTINCT u.user_id) gives 200. Multiply by 100.0 before dividing and round to two places, and you get 43.0.
SELECT ROUND(100.0 * COUNT(DISTINCT cv.user_id) / COUNT(DISTINCT u.user_id), 2) AS viewer_pct
FROM users u
LEFT JOIN content_views cv ON u.user_id = cv.user_id- Inner join
users JOIN content_viewskeeps only users who have a view. Both counts come out to 86, so the ratio is a flat 100%: a number that looks like a win and means nothing.- Left join from users
users LEFT JOIN content_viewskeeps all 200 users. The numerator is still 86, the denominator is 200, and the ratio is 43.0%.
Common follow-up questions
- How would you compute this per `age_bucket`? (Tests grouping the same ratio, and noticing that users with a `NULL` bucket become their own row.)
- What if some `user_id` values in `content_views` are not in `users`? (Tests whether the numerator stays limited to registered users, which the left join from `users` already guarantees.)
- How would you make this a monthly time series of adoption? (Tests where the month filter belongs: in the join condition, so never-viewers in that month stay in the base.)