Top Content by Watch Time

A medium SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
medium
Seniority
L4
Asked in
BeyondTrust

Problem

A media platform wants to know which content holds the most attention among its signed-in audience, so it totals watch time only from views tied to a logged-in viewer. To keep untracked traffic off the board, it also drops any content that never drew a view from a recognized device. Return the content in the top three tiers by total watch time, where items sharing a total belong to the same tier, showing the content ID, title, and total watch time, highest first.

Some content holds attention. Others get skipped.

Tables

content_items

Columns: content_id INTEGER, title TEXT, content_type TEXT, duration_seconds INTEGER, creator_id INTEGER, publish_date TEXT

content_items: 10 of 200 rows
content_idtitlecontent_typeduration_secondscreator_idpublish_date
243The Ultimate Guide to AI TrendsarticleNULL1972026-02-02
286Top 10 Mobile Appspodcast1542942025-03-03
329Understanding Machine Learning [DRAFT]short2013912026-04-04
372Mastering Tech Startupslivestream2484882025-05-05
415Introduction to Cybersecurityvideo2955852026-06-06
458Deep Dive: Cloud ComputingarticleNULL6822025-07-07
501Behind the Scenes of Product Managementpodcast3897792026-08-08
544Breaking Down DevOpsshort4368762025-09-09
587Exploring Web Developmentlivestream483NULL2026-01-10
630How to Data Sciencevideo53010702025-11-11

content_views

Columns: view_id INTEGER, content_id INTEGER, user_id INTEGER, device_id INTEGER, viewed_at TEXT, watch_seconds INTEGER

content_views: 10 of 200 rows
view_idcontent_iduser_iddevice_idviewed_atwatch_seconds
705337213614042026-02-02 01:07:0041
710650126226072026-03-03 02:14:0072
715963038838102026-04-04 03:21:00103
7212759514410132026-05-05 04:28:00134
7265888640512162026-06-06 05:35:00165
73181017766614192026-07-07 06:42:00196
73711146NULL16222026-08-08 07:49:00227
7424127548818252026-09-09 08:56:00258
747714041749NULL2026-01-10 09:03:00289
75301533301022312026-02-11 10:10:00320

Expected output

First 3 of 3 rows
content_idtitlelifetime_value
1877Exploring Web Development13534
2780How to Data Science10776
3382Mastering Tech Startups10146

Worked solution and explanation

What this problem is really testing

Strip the 'watch time' costume and this is a scoped aggregate ranked into tie-tiers, guarded by two quality gates. The skill: sum watch time per content, but only over views that survive both gates, then return whole tiers of totals rather than a fixed row count. Anyone can write the join and the SUM. Three things separate candidates. First, the scope: count anonymous views (user_id is null) and every total inflates, so the wrong content floats up. Second, the untracked gate: content whose views never carry a device_id is traffic you cannot trust, and it has to be dropped after grouping, which is exactly what a COUNT-based HAVING is for. Third, the tie semantics: 'top three tiers including ties' rules out LIMIT 3 and ROW_NUMBER, which both cut a tied third tier. Miss any one and your answer is confidently wrong.


Building it up

Step 1: Scope to signed-in views

The metric is watch time from the signed-in audience, so add WHERE cv.user_id IS NOT NULL. This is not decorative: about a tenth of views are anonymous and they carry real watch seconds, so dropping this filter changes totals and can reorder the top three. Note it filters the VIEWER, not the duration; a signed-in view can still have a null watch_seconds, and SUM simply ignores that value.

Step 2: Aggregate per content

First CTE: INNER JOIN content_items to content_views on content_id, then GROUP BY ci.content_id, ci.title and SUM(cv.watch_seconds) AS lifetime_value. Carry title in the group key so you can return it without a second pass over the dimension table.

Step 3: Drop untracked content

A content whose every view has a null device_id is untracked traffic you should not rank. That is a condition on the GROUP, not on one row, so it belongs in HAVING COUNT(cv.device_id) >= 1: COUNT ignores nulls, so a content with no recognized device counts zero and is dropped. Do this in HAVING, not WHERE: a per-view device_id IS NOT NULL would keep only some of a content's views and mis-total the rest.

Step 4: Rank the totals into tiers

Second CTE: DENSE_RANK() OVER (ORDER BY lifetime_value DESC). Dense, not regular RANK: regular RANK skips numbers after a tie, so two items tied at tier 1 push RANK to 3 and tier <= 3 would grab a fourth-place item while pretending it saw three tiers. DENSE_RANK gives 1, 1, 2, so three tiers means three genuine total levels.

Step 5: Keep the top three tiers, ordered

Outer SELECT ... WHERE tier <= 3, then ORDER BY lifetime_value DESC, content_id so the output is deterministic even when two items tie. This is the only place <= 3 appears; do not also add LIMIT 3 or you reintroduce the truncation you just avoided.


The solution

The Ones Who Hold Attention
WITH content_stats AS (
  SELECT
    ci.content_id,
    ci.title,
    SUM(cv.watch_seconds) AS lifetime_value
  FROM content_items ci
  INNER JOIN content_views cv ON ci.content_id = cv.content_id
  WHERE cv.user_id IS NOT NULL
  GROUP BY ci.content_id, ci.title
  HAVING COUNT(cv.device_id) >= 1
),
ranked AS (
  SELECT
    content_id,
    title,
    lifetime_value,
    DENSE_RANK() OVER (ORDER BY lifetime_value DESC) AS tier
  FROM content_stats
)
SELECT content_id, title, lifetime_value
FROM ranked
WHERE tier <= 3
ORDER BY lifetime_value DESC, content_id

Common follow-up questions

Common follow-up questions