The Hours They Stayed

A medium Spark interview practice problem on DataDriven. Write the Spark code and run it against the tables, with instant feedback.

Domain
Spark
Difficulty
medium
Seniority
junior
Asked in
Tata Consultancy Services

Problem

Each row of content_views is one play of a catalog item, carrying who watched and how many seconds they watched, while the item's format lives in content_items. Report the total watch time and the unique viewer count of every content type that drew more than 28 unique viewers.

Tables

content_views

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

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

content_items

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

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

Expected output

First 4 of 4 rows
content_typetotal_watch_secondsunique_viewers
article5791630
livestream5567237
short5989029
video6058329

Worked solution and explanation

What this problem is really testing

This is a per-format audience count dressed up as a watch-time report, and the whole answer turns on one word: unique. Every row of content_views is a play, not a person. The same user_id shows up on several plays and several device_ids, and 28 plays carry no user_id at all. Count rows and every format looks bigger than it is. On this seed podcast sits at exactly 28 unique viewers, so a row count or a >= 28 slips it past a bar it does not clear. The second trap pulls the other way: drop the anonymous plays, or deduplicate plays before summing, and the watch-time total shrinks, because those seconds were really watched.


Build it in three moves

Step 1: Attach the format from content_items

watch_seconds and user_id live on the play; content_type lives on the catalog row. Inner-join on content_id so every play picks up its format. The catalog holds one row per content_id, so the join never duplicates a play and the watch-time sum stays honest.

Step 2: Collapse to one row per content_type

Compute F.sum("watch_seconds") and F.countDistinct("user_id") in the same agg. countDistinct skips the NULL viewers on its own while sum still adds their seconds. That is exactly the split the business wants: anonymous time counts, but anonymous plays add no audience. Name the result per_type.

Step 3: Hold the bar against unique_viewers

The 'more than 28' rule is about a per-format figure that does not exist until after the agg, so the filter goes on per_type. That is the DataFrame API's HAVING. Put it before the groupBy and there is no unique_viewers column to test.


The solution

Aggregate first, then hold the bar
from pyspark.sql import functions as F

views = spark.table("content_views")
items = spark.table("content_items")

per_type = (
    views
    .join(items, "content_id")
    .groupBy("content_type")
    .agg(
        F.sum("watch_seconds").alias("total_watch_seconds"),
        F.countDistinct("user_id").alias("unique_viewers"),
    )
)

result = per_type.filter(F.col("unique_viewers") > 28)

The bar is tested on per_type, after the agg has produced unique_viewers.

Counting plays
F.count("user_id") counts every play with a viewer attached. Repeat plays by one person, or one person on two devices, each add one. Every format's audience is overstated, and podcast can clear a bar it never reached.
Counting people
F.countDistinct("user_id") counts each person once per format, whatever the device and however many replays. Podcast lands on 28 and is correctly left out; the other four formats keep their true audience.
Spark broadcasts the small catalog on its own; the one real shuffle is the aggregation on `content_type`.
  • plays (source)
  • catalog (source)
  • attach format (transform, Spark)
  • per-format totals (transform, Spark)
  • audience bar (transform, Spark)
  • result (consumer)
  • plays -> attach format
  • catalog -> attach format
  • attach format -> per-format totals
  • per-format totals -> audience bar
  • audience bar -> result

Common follow-up questions