Every Door They Opened

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
Spotify

Problem

How many minutes of session time in user_sessions, rounded to the nearest minute, and how many different pages in page_views has each user logged since 2026 began? The analyst wants a row for everyone who has ever appeared in either log, so a user whose activity all predates 2026 still shows up with zeros, while page views with no user id are anonymous traffic that belongs to no one. Page addresses that differ only in letter case are the same page.

Add up the hours, count the rooms they walked through.

Tables

user_sessions

Columns: session_id INTEGER, user_id INTEGER, device_id INTEGER, session_start TEXT, session_duration_sec INTEGER, pages_viewed INTEGER

user_sessions: 10 of 200 rows
session_iduser_iddevice_idsession_startsession_duration_secpages_viewed
70611972302026-02-02 01:00:00533
71222942592026-03-03 02:00:00766
71833912882026-04-04 03:00:00999
72444883172026-05-05 04:00:0012212
73055853462026-06-06 05:00:0014515
73666823752026-07-07 06:00:0016818
74277794042026-08-08 07:00:0019121
7488876NULL2026-09-09 08:00:0021424
75499734622026-01-10 09:00:0023727
761010704912026-02-11 10:00:0026030

page_views

Columns: view_id INTEGER, page_url TEXT, user_id INTEGER, referrer TEXT, dur_ms INTEGER, device TEXT, viewed_at TEXT

page_views: 10 of 200 rows
view_idpage_urluser_idreferrerdur_msdeviceviewed_at
5053/products1555amazon.com637desktop2026-09-02 01:09:00
5106/checkout1652twitter.com774tablet2026-09-03 02:18:00
5159/profile1749github.com911Mobile2026-09-04 03:27:00
5212/search1846NULL1048DESKTOP2026-09-05 04:36:00
5265/about1943reddit.com1185mobile2026-09-06 05:45:00
5318/blog/intro1555NULLNULLdesktop2026-09-07 06:54:00
5371/pricing1652bing.com1459tablet2026-09-08 07:03:00
5424/docs/api1749news.ycombinator.com1596Mobile2026-09-09 08:12:00
5477/settings1846NULL1733DESKTOP2026-09-10 09:21:00
5530/Home1943bing.com1870mobile2026-09-11 10:30:00

Expected output

First 5 of 59 rows
user_idtotal_minutesunique_content_count
1003642
197812
294832
391850
488442

Worked solution and explanation

The disguise and the trap

This is a two-source reconciliation wearing an engagement-report costume, and the twist is that the roster and the numbers live on different clocks. The roster is everyone who has ever appeared in either log; the metrics only count activity since 2026 began. Join the raw tables and a user with 10 sessions and 100 page views explodes into 1,000 rows, so the duration sum lands 100x high and the page count 10x high. Anchor on one table, or let the date filter leak into the roster, and every dormant user and every view-only user quietly vanishes. The fix: build the roster from the union of both logs, collapse each log to one row per user since the start of the year, then left join and zero-fill.

Two independent event logs joined only by `user_id`. Neither is a master user list, so the roster has to come from both.
  • user_sessions: session_id INTEGER PK, user_id INTEGER FK, session_duration_sec INTEGER
  • page_views: view_id INTEGER PK, user_id INTEGER FK, page_url TEXT

Break down the requirements

Step 1: Build the full user roster

Union user_id from all of user_sessions and all of page_views, with no date filter: the roster is who has ever shown up, not who was active this year. UNION removes duplicates, so each user lands once. Only the page-view branch needs user_id IS NOT NULL: that log carries anonymous traffic, and without the guard the roster grows a phantom user whose id is missing.

Step 2: Sum session duration per user

SUM(session_duration_sec) over sessions with session_start on or after the first of 2026, grouped by user_id, then ROUND(total_secs / 60.0). Divide by 60.0 rather than 60 so rounding acts on the exact value. The window lives here and not on the roster: a user whose sessions are all from earlier years still belongs in the report, they just have no row on this side.

Step 3: Count unique pages per user, case folded

COUNT(DISTINCT LOWER(page_url)) over page views since the start of 2026, grouped by user_id. The uniqueness is load-bearing: the same URL opened five times is one page, not five. So is the case fold: the log writes '/Home' next to '/home' and '/PRODUCTS' next to '/products', and a raw count credits those readers with a page they never saw. Forget the window on this side and long-time readers get credit for pages they opened years ago.

Step 4: Left join both aggregates and zero-fill

Left join both yearly sides onto the roster and COALESCE each metric to 0. A user dormant all year has no session row and no page row, and shows 0 and 0 instead of disappearing. An inner join would keep only users active on both sides this year.


The solution

All-time roster, yearly aggregates, zero-filled
SELECT
    roster.user_id,
    COALESCE(ROUND(sessions_this_year.total_secs / 60.0), 0) AS total_minutes,
    COALESCE(views_this_year.unique_pages, 0) AS unique_content_count
FROM (
    SELECT user_id
    FROM user_sessions
    UNION
    SELECT user_id
    FROM page_views
    WHERE user_id IS NOT NULL
) AS roster
LEFT JOIN (
    SELECT user_id, SUM(session_duration_sec) AS total_secs
    FROM user_sessions
    WHERE session_start >= '2026-01-01'
    GROUP BY user_id
) AS sessions_this_year
    ON roster.user_id = sessions_this_year.user_id
LEFT JOIN (
    SELECT user_id, COUNT(DISTINCT LOWER(page_url)) AS unique_pages
    FROM page_views
    WHERE viewed_at >= '2026-01-01'
    GROUP BY user_id
) AS views_this_year
    ON roster.user_id = views_this_year.user_id

Common follow-up questions