Overlapping User Sessions

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
Vanguard

Problem

Each row of user_sessions is one session that starts at session_start and lasts session_duration_sec seconds; no end time is stored. Find every pair of two different sessions from the same user that overlap, meaning each starts before the other ends, and return the user_id with both session_ids and both start times.

Two sessions, one user, same clock. Something overlaps.

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

Expected output

First 3 of 3 rows
user_idsession_id_1session_id_2start_1start_2
10013161132222026-06-15 10:00:002026-06-15 11:00:00
10013161132832026-06-15 10:00:002026-06-15 11:30:00
10013222132832026-06-15 11:00:002026-06-15 11:30:00

Worked solution and explanation

This is an all-pairs interval overlap dressed up as session analytics. Nothing stores when a session ends, so the first move is building the end from session_start plus session_duration_sec seconds. The real test is that overlap relates every session to every other session of the same user, not just to its neighbour. On this data, user 100's 10:00 session runs until noon and overlaps both the 11:00 and the 11:30 session. A sort-and-compare-the-next-row approach finds two pairs and silently drops the third. The self-join gets all the pairs. The trap is the join key: write a.session_id <> b.session_id and every pair comes back twice, mirrored.

Walking to the answer

Step 1: Derive the end that is not stored

datetime(session_start, '+' || session_duration_sec || ' seconds') produces the end in the same 'YYYY-MM-DD HH:MM:SS' text as session_start, so the two compare correctly as strings. A row whose session_duration_sec is NULL gets a NULL end and fails every comparison. That is the right outcome: a session of unknown length cannot be shown to overlap anything.

Step 2: Pair each session with its same-user peers, once

Self-join user_sessions on user_id with a.session_id < b.session_id. That one predicate does two jobs: it removes the session-with-itself row, and it keeps only one of the two mirrored orderings. The preview confirms the convention: the smaller id always sits in session_id_1.

Step 3: Test overlap from both sides

Keep the pair when a.session_start is before b's end and b.session_start is before a's end. The strict < means two sessions that only touch (one ends exactly as the other starts) do not count. The test is symmetric, which matters because a smaller session_id does not promise an earlier start.

Step 4: Return the pair, not the intervals

Project user_id, both ids as session_id_1 and session_id_2, and both starts as start_1 and start_2. The derived ends were only needed for the test.

Self-join with a two-sided overlap test
SELECT
    a.user_id,
    a.session_id AS session_id_1,
    b.session_id AS session_id_2,
    a.session_start AS start_1,
    b.session_start AS start_2
FROM user_sessions a
JOIN user_sessions b
  ON a.user_id = b.user_id
  AND a.session_id < b.session_id
WHERE a.session_start < datetime(b.session_start, '+' || b.session_duration_sec || ' seconds')
  AND b.session_start < datetime(a.session_start, '+' || a.session_duration_sec || ' seconds')
One-directional test
b.session_start BETWEEN a.session_start AND a_end only catches b starting inside a. When the lower-id session starts later, the overlap is missed. BETWEEN is also inclusive, so touching sessions are wrongly flagged. On this seed the lower id always starts first, so the bug hides until real data arrives.
Two-sided test
a.session_start < b_end AND b.session_start < a_end is the negation of 'one ends before the other starts'. It catches containment, partial overlap and equal starts in either order, and it rejects touching sessions.

Common follow-up questions