Skip to content
About
Main content

Traversal and degree skew

Why does traversal cost explode with depth, and why is degree skew the central mechanism?

Traversal as a query primitive

A traversal is a sequence of expansions. The frontier is a set of vertices. Expansion replaces it with the union of their neighbours, minus what you have already seen. That "minus what you have seen" is not a detail. It is a stateful deduplication against the visited set, and it is why traversal cannot be written as a fixed pipeline: you do not know in advance how many iterations you need.

SQL has a feature for this, the recursive query (WITH RECURSIVE): a query that feeds its own output back in as input until no new rows appear. Graph traversal is (repeat a step until the result stops changing), and recursive SQL is the same mechanism behind a different interface. They are not two features.

-- Everyone reachable from user 7 within 3 hops (UNION removes already-seen pairs).
WITH RECURSIVE reach(user_id, depth) AS (
    SELECT 7, 0
  UNION
    SELECT f.followee_id, r.depth + 1
    FROM reach r JOIN follows f ON f.follower_id = r.user_id
    WHERE r.depth < 3
)
SELECT DISTINCT user_id FROM reach;

Frontier size is the cost, and it is governed by the branching factor. With average degree d (the number of edges at a vertex), hop k touches on the order of d^k vertices before deduplication.

average edges at a vertex
hops traversed
from one endbidirectional
Data table
Vertices touched by depth (average degree 100)
Hops kseriesVertices touched
1from one end100
1.06from one end133
1.13from one end178
1.19from one end237
1.25from one end316
1.31from one end422
1.38from one end562
1.44from one end750
1.5from one end1K
1.56from one end1.33K
1.63from one end1.78K
1.69from one end2.37K
1.75from one end3.16K
1.81from one end4.22K
1.88from one end5.62K
1.94from one end7.5K
2from one end10K
2.06from one end13.3K
2.13from one end17.8K
2.19from one end23.7K
2.25from one end31.6K
2.31from one end42.2K
2.38from one end56.2K
2.44from one end75K
2.5from one end100K
2.56from one end133K
2.63from one end178K
2.69from one end237K
2.75from one end316K
2.81from one end422K
2.88from one end562K
2.94from one end750K
3from one end1M
3.06from one end1.33M
3.13from one end1.78M
3.19from one end2.37M
3.25from one end3.16M
3.31from one end4.22M
3.38from one end5.62M
3.44from one end7.5M
3.5from one end10M
3.56from one end13.3M
3.63from one end17.8M
3.69from one end23.7M
3.75from one end31.6M
3.81from one end42.2M
3.88from one end56.2M
3.94from one end75M
4from one end100M
4.06from one end133M
4.13from one end178M
4.19from one end237M
4.25from one end316M
4.31from one end422M
4.38from one end562M
4.44from one end750M
4.5from one end1B
4.56from one end1.33B
4.63from one end1.78B
4.69from one end2.37B
4.75from one end3.16B
4.81from one end4.22B
4.88from one end5.62B
4.94from one end7.5B
5from one end10B
5.06from one end13.3B
5.13from one end17.8B
5.19from one end23.7B
5.25from one end31.6B
5.31from one end42.2B
5.38from one end56.2B
5.44from one end75B
5.5from one end100B
5.56from one end133B
5.63from one end178B
5.69from one end237B
5.75from one end316B
5.81from one end422B
5.88from one end562B
5.94from one end750B
6from one end1,000B
1bidirectional20
1.06bidirectional23.1
1.13bidirectional26.7
1.19bidirectional30.8
1.25bidirectional35.6
1.31bidirectional41.1
1.38bidirectional47.4
1.44bidirectional54.8
1.5bidirectional63.2
1.56bidirectional73
1.63bidirectional84.3
1.69bidirectional97.4
1.75bidirectional112
1.81bidirectional130
1.88bidirectional150
1.94bidirectional173
2bidirectional200
2.06bidirectional231
2.13bidirectional267
2.19bidirectional308
2.25bidirectional356
2.31bidirectional411
2.38bidirectional474
2.44bidirectional548
2.5bidirectional632
2.56bidirectional730
2.63bidirectional843
2.69bidirectional974
2.75bidirectional1.12K
2.81bidirectional1.3K
2.88bidirectional1.5K
2.94bidirectional1.73K
3bidirectional2K
3.06bidirectional2.31K
3.13bidirectional2.67K
3.19bidirectional3.08K
3.25bidirectional3.56K
3.31bidirectional4.11K
3.38bidirectional4.74K
3.44bidirectional5.48K
3.5bidirectional6.32K
3.56bidirectional7.3K
3.63bidirectional8.43K
3.69bidirectional9.74K
3.75bidirectional11.2K
3.81bidirectional13K
3.88bidirectional15K
3.94bidirectional17.3K
4bidirectional20K
4.06bidirectional23.1K
4.13bidirectional26.7K
4.19bidirectional30.8K
4.25bidirectional35.6K
4.31bidirectional41.1K
4.38bidirectional47.4K
4.44bidirectional54.8K
4.5bidirectional63.2K
4.56bidirectional73K
4.63bidirectional84.3K
4.69bidirectional97.4K
4.75bidirectional112K
4.81bidirectional130K
4.88bidirectional150K
4.94bidirectional173K
5bidirectional200K
5.06bidirectional231K
5.13bidirectional267K
5.19bidirectional308K
5.25bidirectional356K
5.31bidirectional411K
5.38bidirectional474K
5.44bidirectional548K
5.5bidirectional632K
5.56bidirectional730K
5.63bidirectional843K
5.69bidirectional974K
5.75bidirectional1.12M
5.81bidirectional1.3M
5.88bidirectional1.5M
5.94bidirectional1.73M
6bidirectional2M

This has a clean architectural consequence. Shallow traversal from few starting points is an OLTP workload (many small, fast, interactive operations). Anything else is an OLAP workload (few large scans over all the data). They want opposite systems: pointer-chasing over mutable adjacency, versus scan-shaped iteration over immutable . Treating "graph" as one workload is the source of most disappointment in this family.

Pattern matching (finding a small shape, such as a triangle of three mutually-following users, inside a big graph) is a third cost regime, different from traversal. It is where worst-case-optimal join algorithms live: multiway join methods whose running time is bounded by the largest possible output size. They matter because ordinary pairwise joins can build huge intermediate results on triangle-like patterns.

BFS and DFS differ in memory shape, not in edges touched

Breadth-first (BFS)Depth-first (DFS)
What it holdsthe whole frontier, up to about d^k verticesone path of depth k (tiny)
Parallel?yes: each level is synchronous, so it can be split across workers and machinesinherently sequential
Typical useevery distributed graph systemsingle-machine pattern matchers

Parallelism costs memory here. This is the general trade between ordering and parallelism, appearing in its memory-shaped form.

Bidirectional search deserves one line, because it is the only real asymptotic win available. Search k/2 hops from both ends and meet in the middle: the cost is 2·d^(k/2) instead of d^k. With d = 100 and k = 4: 2 × 100² = 20,000 instead of 100,000,000. It needs both endpoints, so it serves shortest-path queries and nothing else.

One line: traversal is fixpoint iteration whose cost is exponential in depth until it saturates the component. Shallow-from-few is OLTP; everything else is a disguised full scan.


Degree skew: the real central mechanism

Real graphs are power-law (also called heavy-tailed): a handful of vertices have degrees in the millions, while the median vertex has a handful. (This is the Zipf shape: a few items take most of the weight.) This is not a data-quality problem to be cleaned. It is the empirical shape of essentially every graph anyone cares about, including Haatly's follower graph.

It breaks partitioning, and the common explanation is wrong. The problem is not that balanced graph partitioning is NP-hard (computationally intractable in the worst case). Approximation algorithms exist and would be fine. The problem is that in a power-law graph there is no good balanced cut to find. The graph has no sparse separators, so every balanced partition cuts an enormous fraction of edges. Better partitioners cannot help, because they are searching for something that does not exist.

That forces a real change of representation:

Edge-cutVertex-cut
Unit assigned to a machineeach vertex (with its edges)each edge
What is splitedges across the boundaryhigh-degree vertices, which are replicated on every machine holding one of their edges
Hub with 2 million edgesone machine owns all of them and becomes the bottleneckits edges spread over all machines
Pricehot spotsreplication, plus reconciling replica state every round

Vertex-cut balances load on edges, which is what the work is actually proportional to.

Notice the shape of that resolution: when one entity is too large to place, split it and reconcile. It is the same move as salting a skewed join key (appending a random suffix so one hot key becomes several smaller keys, then merging the partial results), as sharding a hot partition, and as splitting a hot key in a stream. The constant cost is the same every time: an extra step that the unskewed case does not need.

Skew also makes the average degree nearly useless for cost estimation. A traversal's cost is dominated by whether it hit a hub, which depends on the data and not on the query. This is variance costing more than volume, in its strongest form here: the variance of degree, not the mean, determines both latency and the failure mode, and a query planner with only average statistics will be wrong in the direction that matters.

One line: power-law graphs have no sparse balanced cuts to find, so you replicate hubs and reconcile.