Session Page View Distance
A hard SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- hard
- Seniority
- L5
- Asked in
Problem
A session's first and last page views are the ones with the lowest and highest view_id, and a session with only one view, or whose first or last view has no recorded dur_ms, has no distance to measure. Linking page_views to user_sessions through user_id, the UX research team wants the average across sessions of the last view's dur_ms minus the first view's dur_ms.
Page view distance per session.
Tables
page_views
Columns: view_id INTEGER, page_url TEXT, user_id INTEGER, referrer TEXT, dur_ms INTEGER, device TEXT, viewed_at TEXT
| view_id | page_url | user_id | referrer | dur_ms | device | viewed_at |
|---|---|---|---|---|---|---|
| 5053 | /products | 1555 | amazon.com | 637 | desktop | 2026-09-02 01:09:00 |
| 5106 | /checkout | 1652 | twitter.com | 774 | tablet | 2026-09-03 02:18:00 |
| 5159 | /profile | 1749 | github.com | 911 | Mobile | 2026-09-04 03:27:00 |
| 5212 | /search | 1846 | NULL | 1048 | DESKTOP | 2026-09-05 04:36:00 |
| 5265 | /about | 1943 | reddit.com | 1185 | mobile | 2026-09-06 05:45:00 |
| 5318 | /blog/intro | 1555 | NULL | NULL | desktop | 2026-09-07 06:54:00 |
| 5371 | /pricing | 1652 | bing.com | 1459 | tablet | 2026-09-08 07:03:00 |
| 5424 | /docs/api | 1749 | news.ycombinator.com | 1596 | Mobile | 2026-09-09 08:12:00 |
| 5477 | /settings | 1846 | NULL | 1733 | DESKTOP | 2026-09-10 09:21:00 |
| 5530 | /Home | 1943 | bing.com | 1870 | mobile | 2026-09-11 10:30:00 |
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
| avg_distance |
|---|
| 5883.493150684932 |
Worked solution and explanation
This is positional extraction dressed up as engagement research. The question is not how long the longest view ran; it is what dur_ms sat on the lowest and the highest view_id in each session. Anyone can join page_views to user_sessions. What separates candidates is refusing to answer with MAX(dur_ms) - MIN(dur_ms): those are the extremes of the values, not the values at the endpoints. Because aggregates skip NULLs, they quietly measure from the second view when the first one has no recorded duration, which invents a distance for a session the question says cannot be measured.
Getting to the answer
Step 1: Accept the grain the user_id link gives you
The only key between the tables is user_id, so every session inherits all of its user's views, and two sessions of the same user end up with identical endpoints. That is the stated linkage, not something to fix here. Say it out loud anyway: a user with five sessions counts five times in the average, and an interviewer is listening for whether you noticed.
Step 2: Number each session's views from both ends
Write two ROW_NUMBER() windows over PARTITION BY us.session_id, one ordered by pv.view_id ASC and one by DESC. The row with rn_asc = 1 is the first view and the row with rn_desc = 1 is the last, whatever their durations are. view_id is unique, so neither numbering has a tie to break.
Step 3: Count the views without collapsing them
COUNT(*) OVER (PARTITION BY us.session_id) stamps the session size on every row. The single-view guard then rides along with the endpoint rows instead of needing a second pass. Leave the count window unordered: it should cover the whole session.
Step 4: Pair the endpoints and subtract in order
Join page_ranked to itself on session_id, keeping the rn_asc = 1 row as f and the rn_desc = 1 row as l, and compute l.dur_ms - f.dur_ms. Leave NULLs alone. If either endpoint has no duration, the distance is NULL and AVG leaves that session out, which is exactly what the question asks.
WITH page_ranked AS (
SELECT pv.user_id, us.session_id, pv.dur_ms, pv.view_id,
ROW_NUMBER() OVER (PARTITION BY us.session_id ORDER BY pv.view_id ASC) AS rn_asc,
ROW_NUMBER() OVER (PARTITION BY us.session_id ORDER BY pv.view_id DESC) AS rn_desc,
COUNT(*) OVER (PARTITION BY us.session_id) AS view_count
FROM page_views pv
INNER JOIN user_sessions us ON pv.user_id = us.user_id
),
session_dist AS (
SELECT f.session_id,
l.dur_ms - f.dur_ms AS distance
FROM page_ranked f
INNER JOIN page_ranked l ON f.session_id = l.session_id
WHERE f.rn_asc = 1 AND l.rn_desc = 1
AND f.view_count > 1
)
SELECT AVG(distance) AS avg_distance FROM session_dist- Extremes of the values
MAX(dur_ms) - MIN(dur_ms)per session. It is never negative, and it skips NULLs, so a session whose first view has no duration is measured from its second view and counted.- Values at the endpoints
l.dur_ms - f.dur_mson thern_desc = 1andrn_asc = 1rows. It keeps its sign, and a missing endpoint makes the session unmeasurable, so it drops out of the average.
Common follow-up questions
- How would you limit each session to its own views using `session_start` and `session_duration_sec`? (Tests turning the fan-out join on `user_id` into a time-range join.)
- What changes if the team wants the absolute gap rather than last minus first? (Tests reading the sign of the metric as a business decision.)
- How would you weight the average per user instead of per session? (Tests noticing that the `user_id` link counts each user once per session.)