Six Degrees
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
- Meta
Problem
In chat_msgs, every reply ties its sender to the person in reply_to, and the tie runs both ways, though nobody is ever tied to themselves. How many different people is each person tied to, and what percentage of everyone who has ever sent or received a message does that make, rounded to two decimals?
Every reply ties two names together. Find whose web reaches the furthest.
Tables
chat_msgs
Columns: msg_id INTEGER, channel TEXT, sender_id INTEGER, content TEXT, msg_type TEXT, sent_at TEXT, edited INTEGER, reply_to INTEGER
| msg_id | channel | sender_id | content | msg_type | sent_at | edited | reply_to |
|---|---|---|---|---|---|---|---|
| 147 | #engineering | 197 | Anyone seeing increased latency? | file | 2026-02-02 01:07:00 | 0 | NULL |
| 194 | #incidents | 294 | PR #412 ready for review | reaction | 2026-03-03 02:14:00 | 0 | NULL |
| 241 | #random | 391 | Dashboard is down | thread_reply | 2026-04-04 03:21:00 | 0 | NULL |
| 288 | #deployments | 488 | Fixed the flaky test | system | 2026-05-05 04:28:00 | 0 | NULL |
| 335 | #data-team | 585 | Merged the migration | Text | 2026-06-06 05:35:00 | 0 | NULL |
| 382 | #general | 682 | NULL | FILE | 2026-07-07 06:42:00 | 0 | NULL |
| 429 | #engineering | 779 | LGTM | text | 2026-08-08 07:49:00 | 0 | NULL |
| 476 | #incidents | 876 | 👍 | file | 2026-09-09 08:56:00 | 0 | 5 |
| 523 | #random | 973 | reaction | 2026-01-10 09:03:00 | 0 | NULL | |
| 570 | #deployments | 1070 | Can someone check the pipeline? | thread_reply | 2026-02-11 10:10:00 | 0 | NULL |
Expected output
| user_id | unique_connections | connection_score |
|---|---|---|
| 5 | 1 | 1.35 |
| 11 | 1 | 1.35 |
| 14 | 1 | 1.35 |
| 17 | 1 | 1.35 |
| 23 | 1 | 1.35 |
Worked solution and explanation
What this problem really is
This is a graph degree problem wearing a chat log's clothes. Each reply is one undirected edge between two user IDs, and the connection score is just a node's degree divided by the number of nodes in the graph. The trap sits in one column: reply_to looks like it should point back at a message, but here it carries the other person's user id directly. Read it as a message pointer and join chat_msgs to itself on msg_id, and on this data nothing matches, because reply_to values like 5 never line up with msg_id values in the millions, so the result comes back empty and every user vanishes.
Building the edge list
Step 1: Turn replies into undirected edges
Keep only replies that point at someone other than the sender. reply_to <> sender_id does both jobs at once: a comparison against a NULL reply_to is never true, so plain messages drop out along with the self-replies. Emit each kept row twice, sender_id to reply_to and back, and stack the two with UNION ALL. A pair who replied to each other, or kept replying, now shows up more than once, and that is fine: the next step is where it collapses.
Step 2: Measure the whole graph
The denominator is every person who ever sent or received a message, not just the senders. Union the senders with the reply targets and count the rows. Here the reply_to IS NOT NULL guard is load-bearing: most messages are not replies, and without it the union carries one NULL row that COUNT(*) happily counts as an extra person, quietly shrinking every score.
Step 3: Degree over platform size
Group the edge list by user_id and take COUNT(DISTINCT connected_to) as each person's degree. A plain COUNT(*) here counts replies, not people: two people who traded replies would each be credited twice. The platform total is a single scalar, so a CROSS JOIN broadcasts it to every person cheaply; multiply by 100.0 and round to two places. The question asks for a score per person, not a leaderboard, so no row order is owed.
The solution
WITH edges AS (
SELECT sender_id AS user_id, reply_to AS connected_to
FROM chat_msgs
WHERE reply_to <> sender_id
UNION ALL
SELECT reply_to AS user_id, sender_id AS connected_to
FROM chat_msgs
WHERE reply_to <> sender_id
),
degrees AS (
SELECT user_id, COUNT(DISTINCT connected_to) AS unique_connections
FROM edges
GROUP BY user_id
),
participants AS (
SELECT sender_id AS person_id FROM chat_msgs
UNION
SELECT reply_to AS person_id FROM chat_msgs WHERE reply_to IS NOT NULL
),
platform AS (
SELECT COUNT(*) AS total_users
FROM participants
)
SELECT
d.user_id,
d.unique_connections,
ROUND(d.unique_connections * 100.0 / p.total_users, 2) AS connection_score
FROM degrees d
CROSS JOIN platform p;- Self-join on `msg_id` (wrong here)
- JOIN
chat_msgschild ONchild.reply_to=parent.msg_idassumesreply_tois a message foreign key. On this datareply_toholds a user id, never amsg_id, so the join matches zero rows and the answer is empty. - Pair sender with `reply_to` (correct)
- Treat
reply_toas the connected user's id and pair it withsender_iddirectly. Both orderings give an undirected edge, and counting distinct partners per user gives the degree.
Common follow-up questions
- How would you surface only the ten most connected people? (Now order matters: sort by the score, decide which tied person survives the cut, then apply a `LIMIT`.)
- What changes if you only want connections formed in the last 30 days? (Push a `sent_at` window into the edges CTE before the UNION so both directions inherit it.)
- How would you score mutual connections, where both people replied to each other? (Intersect the two directions instead of unioning them, so an edge survives only if it appears both ways.)
- What if the table held billions of messages? (Precompute and incrementally maintain an edge or degree table rather than rebuilding the graph per query.)