Lesson 05 · The specialist in connections

Graph: when the relationships are the data

Every model so far treats relationships as something you compute: relational finds them by matching values at query time (join), documents freeze one relationship into the tree shape, key-value pretends they don't exist. The graph model's proposal: relationships are data: store them as first-class records with their own type, direction, and properties, physically attached to the nodes they connect. A node holds direct references to its edges; an edge holds direct references to its endpoints. Traversal is pointer-chasing, not index lookup. The literature calls this index-free adjacency, and one lab makes its consequences visceral:

The depth race

"People you may know": 3 hops out, both engines

A social network: ~200 friends per user. Compute friends-of-friends-of-friends for one user in a relational schema (friendships(user_a, user_b)) vs a graph store. Advance one hop at a time and watch the work counters.

depth
0
SQL: rows index-matched
0
Graph: pointers followed
0
ratio
:
depth 0: just you.

Both engines touch a similar number of people: the difference is the cost of each step. The relational engine holds no memory of "who connects to whom" beyond rows in a table: every hop is another self-join, another B-tree probe per person against a friendships table with hundreds of millions of rows, with intermediate result sets materialized in between. The graph engine starts at your node and walks: each friend is one pointer dereference, cost proportional to edges actually traversed, independent of how big the rest of the graph is. Depth is where the models diverge: at hop 1 they tie; every added hop multiplies SQL's pain and merely extends the graph's walk.

Asking graph-shaped questions

Query languages built for traversal make the pattern the syntax. Cypher (Neo4j; now ISO-standard GQL) draws the shape with ASCII art:

MATCH (me:Person {name:'Alex'})-[:FRIEND*1..3]-(candidate:Person) WHERE NOT (me)-[:FRIEND]-(candidate) AND candidate <> me RETURN candidate, count(*) AS mutual ORDER BY mutual DESC LIMIT 10 -- "walk FRIEND edges 1–3 deep; suggest strangers, ranked by paths to them"

Try expressing *1..3: a variable-length path: in SQL: it's three self-joins UNIONed, or a recursive CTE the optimizer handles with a shrug. And "shortest path from A to B"? SQL has no natural sentence for it at all; in a graph store it's a built-in. Explore what path queries feel like:

Traverse a live access graph

An identity-and-access graph (your home turf): users, groups, resources; edges are memberships and grants. Click a user to trace everything they can reach and why: the transitive closure that access-review tools compute.

Notice what the trace did: followed MEMBER_OF and GRANTED edges transitively, through nested groups, collecting paths: because in access questions, the path is the answer ("alex reaches prod-db via admins → infra-team"). Fraud rings ("accounts sharing devices sharing cards"), dependency analysis, recommendations, knowledge graphs: all the same shape: the question is about the connective tissue, at unknown depth, and the justification matters as much as the result. That's the graph database's home terrain.

The honest boundary: graphs are not a better general-purpose database. Bulk analytics ("average age of all users") gain nothing from adjacency; simple 1-hop lookups are served equally well by a join or a document; and sharding a densely-connected graph across machines is notoriously brutal: a traversal that hops nodes now hops networks (the distributed systems course explains exactly why that hurts), which is why graph deployments scale up before they scale out. Species: Neo4j (property graph, Cypher), Amazon Neptune (property graph + RDF), ArangoDB and CosmosDB (multi-model), plus the "graph layer on relational" pattern: recursive CTEs: that carries many real systems further than advertised. Use a graph store when path-shaped queries at depth ≥ 2–3 are the core workload, not a garnish.

Check yourself

1. Why does the relational version of friends-of-friends get slower as the total user base grows, while the graph traversal doesn't (for the same friend counts)?

Index-free adjacency in one sentence: the join is precomputed into the storage layout. A join must ask a global structure "who matches?"; a node just reads its own adjacency. Locality of reference vs global lookup: the same physics as Lesson 3's embedding, applied to relationships instead of children.

2. Your Cyberdesk-style access question: "which users can reach resource R through any chain of group memberships (groups nest arbitrarily deep)?" Which model fits, and why?

"Arbitrary depth" is the trigger word: fixed-depth queries suit joins; unbounded transitive closure suits traversal. The key-value precompute answers fast but must be invalidated on every membership change anywhere in the chain (Lesson 2's hand-rolled-index trap, with security consequences). Document embedding freezes one nesting level. The parenthetical matters too: frequency decides whether the workload deserves its own engine: the theme Lesson 7 turns into a framework.

3. A team stores its entire e-commerce platform (catalog, orders, payments, recommendations) in a graph database because "everything is connected." What's the likely outcome?

The field guide's recurring moral: choose by query shape, not by ontology. All data is connected; the question is whether your queries traverse connections at depth. The standard architecture is relational (or document) as the system of record, with a graph store fed by CDC (replication course, lesson 5) for the path-shaped workloads: polyglot persistence, Lesson 7's subject.