Friends of friends melts a join — the pointer walk that replaces the self-join

In a relational store every extra hop of 'friends of friends' is another self-join of the friendship table, so the intermediate rows explode combinatorially (50 → 2,500 → 6.25M → 312M), while the same question answered by walking from person to person only ever touches the people you actually reach.

Scene 01

Friends of friends melts a join

  1. Watch
  2. Try it
  3. Predict
  4. Capture
Relational: one self-join per hopGraph database: follow pointersUSER_FRIEND(user, friend) · deg 50SELECT f4.friend FROM user_friend…depth d = 1 (friends)intermediate rows5050d12.5Kd2125Kd36.25Md4312Md5AliceBobCarolDaveErinFrankGraceHeidiIvanJudyThe RockThe MatrixInceptionGothamhoppeople you actually reach: 4Neo4j · d4: 1.3sMySQL · d4: 1543sMySQL · d5: did not finishDepth 1: the table self-joins to ~50 rows; the walk reached 4 people.whole-graph lights all
What to watch for

One question: 'who are Alice's friends of friends of friends?' On the LEFT, a relational store answers it by joining the friendship table to itself once per hop — watch the intermediate-row counter as the depth climbs. On the RIGHT, the same question is answered by walking from Alice, person to person, lighting up only the people the walk actually reaches. Watch the two numbers pull apart.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Relational.friendsOfFriends
answering it as a table: each hop joins USER_FRIEND to itself once more
def friends_of_friends(start, depth):
rows = [(start,)] # depth 0: just Alice
for hop in range(depth): # ONE self-join per hop
# join USER_FRIEND again: every row fans to its friends
rows = [(*r, f)
for r in rows
for f in friends_of(r[-1])] # ~degree each
return dedup(last_col(rows)) # ...after materializing rows
Walk.expand
answering it as a walk: step edge by edge, never revisiting a person
def expand(start, depth):
seen = {start}
frontier = [start]
for hop in range(depth): # one step per hop
nxt = []
for person in frontier:
for friend in neighbors(person): # follow an edge
if friend not in seen: # skip the visited
seen.add(friend); nxt.append(friend)
frontier = nxt # only the newly reached
return seen - {start}
cost(depth)
the two cost curves side by side, as the depth knob drives them
def relational_cost(depth):
return DEGREE ** depth # 50^d candidate rows
def walk_cost(depth):
# bounded by distinct people reachable, not 50^d
return len(reachable_within(start, depth))

Where this sits in Build a graph database (Neo4j / Dgraph-style)

Scene 01 of 16, in the Why graphs act — Friends-of-friends melts a join; edges go first-class.. The canonical graph workload — 'friends of friends of friends' — is one self-join per hop, and the intermediate row count explodes combinatorially until a relational engine can't finish; the same query as a pointer walk visits only the people you actually reach.

Up next. The pointer walk won because it treated a friendship as a thing you can FOLLOW, not a row you have to re-join. But we drew that edge by hand — we never said how the data is actually shaped so that an edge becomes followable. Before we can make hops cheap, we need a data model where an edge is a first-class object, not a row in a join table.

All 16 scenes in Build a graph database (Neo4j / Dgraph-style) · Every curriculum

Built with Arqly
Every scene in Build a graph database (Neo4j / Dgraph-style) builds on the one before it.All 16 Build a graph database (Neo4j / Dgraph-style) scenes