Bookends
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
- ESPN
Problem
Our side-by-side of the very first signup and the newest one keeps coming back empty, because both ends of users turn out to be accounts that never logged a session. Counting only people who appear in user_sessions, return every session belonging to the earliest and the most recent signup on one timeline, oldest session first.
The extremes of the user base.
Tables
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 |
users
Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT
| user_id | username | signup_date | account_status | age_bucket | |
|---|---|---|---|---|---|
| 100 | alice | alice@example.com | 2025-02-02 | inactive | 25-34 |
| 197 | aaron42 | aaron42@example.com | 2026-03-03 | suspended | 35-44 |
| 294 | amelia | amelia@example.com | 2024-04-04 | pending_verification | 45-54 |
| 391 | arjun | arjun@example.com | 2025-05-05 | active | 55-64 |
| 488 | ava99 | ava99@example.com | 2026-06-06 | inactive | 65+ |
| 585 | andrew_k | andrew_k@example.com | 2024-07-07 | suspended | NULL |
| 682 | anika | anika@example.com | 2025-08-08 16:34:00 | pending_verification | 18-24 |
| 779 | aiden | aiden@example.com | 2026-09-09 14:53:00 | active | 25-34 |
| 876 | aria_b | aria_b@example.com | 2024-10-10 | inactive | 35-44 |
| 973 | brian | brian@example.com | 2025-11-11 | suspended | 45-54 |
Expected output
| session_id | user_id | device_id | session_start | session_duration_sec | pages_viewed |
|---|---|---|---|---|---|
| 17309 | 8151 | 578 | 2024-08-06 11:00:00 | 1889 | 45 |
| 8647 | 779 | 984 | 2026-01-28 03:00:00 | 651 | 31 |
| 15479 | 8151 | 288 | 2026-02-28 05:00:00 | 959 | 45 |
| 9867 | 779 | 1564 | 2026-03-20 23:00:00 | 1111 | 41 |
| 19139 | 8151 | 868 | 2026-05-12 17:00:00 | 2819 | 45 |
Worked solution and explanation
What this problem really is
This is a min and a max taken over the wrong population, dressed up as an engagement comparison. Anyone can write MIN(signup_date) and MAX(signup_date); the whole question is which rows they run over. Take them over all of users and both ends land on accounts that never logged a session: on the seed the earliest signup is a 2023 account and the latest is a 2026-09-12 account, and neither appears in user_sessions. The join back to sessions then returns nothing, which is exactly the empty comparison the statement complains about. Gate the population first, take the extremes second, then fan the winners back out to their session rows.
How to build it
Step 1: Narrow to users who actually have a session
Test each user with EXISTS against user_sessions and keep their signup_date. This must happen before any boundary is computed: the extremes are defined over this set, not over the raw users table. A semi-join rather than a plain join, because a user with five sessions must still be one row here.
Step 2: Take the earliest signup with a scalar MIN
Compare each session-having user's signup_date to (SELECT MIN(signup_date) FROM session_users). Matching on equality instead of grabbing one row with LIMIT 1 keeps everyone tied on that day. On the seed the earliest session-having signup is user 8151, on 2024-01-01.
Step 3: Take the latest with an anti-join
Left-join session_users to itself on later.signup_date > a.signup_date and keep the rows where later.user_id IS NULL: nobody in the population signed up after them. That is user 779, whose signup_date is '2026-09-09 14:53:00'. The time of day does not hurt: as text it still sorts after every earlier date.
Step 4: Combine, fan out, lay it on one timeline
UNION the two id sets (it collapses a user who is somehow both ends), join that tiny set back to user_sessions, project the six session columns and sort by session_start. The answer is eight rows, five for user 779 and three for user 8151, interleaved by when each session began.
WITH session_users AS (
SELECT u.user_id, u.signup_date
FROM users u
WHERE EXISTS (
SELECT 1
FROM user_sessions s
WHERE s.user_id = u.user_id
)
),
extremes AS (
SELECT su.user_id
FROM session_users su
WHERE su.signup_date = (SELECT MIN(signup_date) FROM session_users)
UNION
SELECT a.user_id
FROM session_users a
LEFT JOIN session_users later
ON later.signup_date > a.signup_date
WHERE later.user_id IS NULL
)
SELECT us.session_id,
us.user_id,
us.device_id,
us.session_start,
us.session_duration_sec,
us.pages_viewed
FROM user_sessions us
JOIN extremes e ON us.user_id = e.user_id
ORDER BY us.session_start;- Boundaries first, filter later
WHERE signup_date = (SELECT MIN(signup_date) FROM users)picks a 2023 account with no sessions, and the join touser_sessionsdrops it. The same happens at the other end with the 2026-09-12 account, so the whole comparison comes back empty.- Filter first, boundaries second
- The
EXISTSgate buildssession_usersbefore anything else, so theMINand the anti-join can only choose people who have sessions to return. User 8151 and user 779 are the answer, and every tied user would come through too.
Common follow-up questions
- Two different users both signed up on the earliest date. How does your query behave, and is that what the business wants? (Tests whether matching on the boundary value, not `LIMIT 1`, is understood as what preserves ties.)
- The team now wants one row per extreme user with their total session count. How does the query change? (Tests changing grain without breaking the population filter.)
- `user_sessions` is partitioned by `session_start` across 365 partitions. Does that help this query, and what would you add to keep it fast? (Tests scaling instincts on a table partitioned by a column the query does not filter on.)