Anchor selection and cardinality

Because expansion is free but the starting fan-out is not, the planner's real job is anchor selection by cardinality — begin from the end that pulls the fewest nodes into memory — and a single WHERE clause that changes which end is most selective will flip the plan's direction entirely.

Previously

Cypher hid one decision: which end of the pattern to start from. The planner makes it by estimating cardinality and anchoring on the few, not the many — and a WHERE clause can flip that choice and reverse the whole walk. But this entire optimization rests on an assumption: that every node has a SMALL, comparable number of edges. What happens when one node has ten million?

Scene 08

Anchor selection and cardinality

  1. Watch
  2. Try it
  3. Predict
  4. Capture
CYPHERAliceBobCarolDaveErinFrankGraceHeidiIvanJudyThe RockThe MatrixInceptionGotham(a:User {name:'Alice'})-[:RATED]->(m:Movie)SPINElocal 2 · global 14Anchoring on the few keeps th…
What to watch for

Same pattern as last scene: Alice's RATED movies. But now look at the two ends. Each one gets a number — an estimate of how many nodes that end pulls into memory. Alice rated about 12 movies. The movie she rated, The Matrix, was rated by about a million people. The chooser compares those two numbers and starts from the SMALLER end — Alice — then expands outward. Step through it.

Continue unlocks when the animation finishes.
Implementation

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

Planner.estimateEnds
the planner's only real cost question: how many nodes does each end pull in?
def estimate_ends(pattern):
# cardinality estimate per BOUND end of the pattern
user_end = stats.out_degree(pattern.user, ':RATED') # Alice ~12
if pattern.where: # WHERE m.title = '...'
movie_end = stats.selectivity(pattern.where) # pins ~1
else:
movie_end = stats.in_degree(pattern.movie, ':RATED') # raters
return {user_end, movie_end} # the numbers on the badges
Planner.pickAnchor
no join planner for an expand — just pick the smaller end, SEEK it once, expand outward
def pick_anchor(pattern):
ends = estimate_ends(pattern)
# anchor on the SMALLEST estimated end — the few, not the many
anchor = argmin(ends, key=estimate)
start = index_seek(anchor) # the one index lookup
# expand AWAY from the anchor; direction follows the choice
return expand(start, ':RATED', toward=other_end(pattern, anchor))

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

Scene 08 of 16, in the The language act — Cypher is a shape; the planner picks an anchor.. The planner's real job is to anchor on the most selective node and estimate cardinality — start from the few, not the many — so flipping a WHERE filter can flip which end is cheapest and reverse the whole traversal direction.

Up next. Anchor selection assumed degrees are roughly comparable — start from the end with the fewest edges. But real graphs have celebrities. The Rock has 232M followers. The instant a traversal has to EXPAND through that node, its relationship chain isn't a quick hop — it's millions of edges to scan, and the O(1)-per-hop promise dies on that one node.

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