Top-Ranked Wines by Variety
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
The product team is looking at content that runs shorter than user 197's typical session. Take user 197's average session length, then return every content item whose duration is at or below that average, walking through them from the lowest content ID up to the highest. For each row, show the literal user ID 197, the content ID, and the content's duration in seconds.
The best bottles. Ranked by variety.
Tables
content_items
Columns: content_id INTEGER, title TEXT, content_type TEXT, duration_seconds INTEGER, creator_id INTEGER, publish_date TEXT
| content_id | title | content_type | duration_seconds | creator_id | publish_date |
|---|---|---|---|---|---|
| 243 | The Ultimate Guide to AI Trends | article | NULL | 197 | 2026-02-02 |
| 286 | Top 10 Mobile Apps | podcast | 154 | 294 | 2025-03-03 |
| 329 | Understanding Machine Learning [DRAFT] | short | 201 | 391 | 2026-04-04 |
| 372 | Mastering Tech Startups | livestream | 248 | 488 | 2025-05-05 |
| 415 | Introduction to Cybersecurity | video | 295 | 585 | 2026-06-06 |
| 458 | Deep Dive: Cloud Computing | article | NULL | 682 | 2025-07-07 |
| 501 | Behind the Scenes of Product Management | podcast | 389 | 779 | 2026-08-08 |
| 544 | Breaking Down DevOps | short | 436 | 876 | 2025-09-09 |
| 587 | Exploring Web Development | livestream | 483 | NULL | 2026-01-10 |
| 630 | How to Data Science | video | 530 | 1070 | 2025-11-11 |
user_sessions
Columns: session_id INTEGER, user_id INTEGER, device_id INTEGER, session_start TEXT, session_duration_sec INTEGER, pages_viewed INTEGER
| session_id | user_id | device_id | session_start | session_duration_sec | pages_viewed |
|---|---|---|---|---|---|
| 7061 | 197 | 230 | 2026-02-02 01:00:00 | 53 | 3 |
| 7122 | 294 | 259 | 2026-03-03 02:00:00 | 76 | 6 |
| 7183 | 391 | 288 | 2026-04-04 03:00:00 | 99 | 9 |
| 7244 | 488 | 317 | 2026-05-05 04:00:00 | 122 | 12 |
| 7305 | 585 | 346 | 2026-06-06 05:00:00 | 145 | 15 |
| 7366 | 682 | 375 | 2026-07-07 06:00:00 | 168 | 18 |
| 7427 | 779 | 404 | 2026-08-08 07:00:00 | 191 | 21 |
| 7488 | 876 | NULL | 2026-09-09 08:00:00 | 214 | 24 |
| 7549 | 973 | 462 | 2026-01-10 09:00:00 | 237 | 27 |
| 7610 | 1070 | 491 | 2026-02-11 10:00:00 | 260 | 30 |
Expected output
| user_id | content_id | duration_seconds |
|---|---|---|
| 197 | 286 | 154 |
| 197 | 329 | 201 |
| 197 | 372 | 248 |
| 197 | 415 | 295 |
| 197 | 501 | 389 |
Worked solution and explanation
One number, two tables that never meet
This is a threshold lookup dressed up as a product question. The skill being probed: can you compute one number from user_sessions and use it to filter content_items, a table it has no relationship to? Anyone can write AVG(...). The trap is reaching for a JOIN because two tables are on the screen. They share no key, so the join either fails or fans out, and each content item gets compared once per session instead of once against the average.
- content_items: content_id PK, title, content_type, duration_seconds, creator_id, publish_date
- user_sessions: session_id PK, user_id, device_id, session_start, session_duration_sec, pages_viewed
Step 1: Compute the threshold as a scalar subquery
SELECT AVG(session_duration_sec) FROM user_sessions WHERE user_id = 197 returns exactly one row and one column. That is what lets it sit on the right side of a comparison. Drop the WHERE and you get the global average, a plausible number that quietly returns the wrong set of content.
Step 2: Filter content_items against it
Put the subquery inside WHERE ci.duration_seconds <= (...). The statement says "at or below", so it is <=, not <. The engine evaluates the uncorrelated subquery once, then scans content_items once.
Step 3: Emit 197 as a literal
user_id does not exist on content_items, and you are not joining to get it. 197 AS user_id stamps the constant on every row, which is exactly what the output asks for.
Step 4: Order by content_id
"Lowest ID up to the highest" is ORDER BY ci.content_id, ascending. The comparison is order sensitive, so this is not optional.
SELECT 197 AS user_id, ci.content_id, ci.duration_seconds
FROM content_items ci
WHERE ci.duration_seconds <= (
SELECT AVG(session_duration_sec) FROM user_sessions WHERE user_id = 197
)
ORDER BY ci.content_idOne pass over user_sessions for the average, one over content_items for the filter.
- The join instinct
- A cross join of
content_itemsanduser_sessionspairs every content row with every session of user 197. A content item can match several sessions and come back several times, and the cutoff becomes each individual session instead of the average. - The scalar subquery
- The subquery collapses user 197's sessions into one number first. Each content row is compared against that single value and appears at most once.
SELECT 197 AS user_id, ci.content_id, ci.duration_seconds
FROM content_items ci
CROSS JOIN user_sessions us
WHERE us.user_id = 197
AND ci.duration_seconds <= us.session_duration_sec
ORDER BY ci.content_idDuplicates content items and compares against single sessions, not the average.
Common follow-up questions
- How would you return this for every user at once? (Needs a per-user average, so the subquery becomes correlated or a grouped CTE, and a join on `user_id` finally appears.)
- What changes if you want content strictly shorter than the average, with `NULL` durations kept in the output? (Tests `<` versus `<=` and explicit `NULL` handling with `COALESCE` or `IS NULL`.)