Relational and document models both assume you mostly know the shape of a query in advance: this table joins that one, this document contains those children.
Some questions do not work that way.
Is there any chain of introductions from me to this person, and how long is it?
You cannot write that as a fixed number of joins, because the answer is the
number of joins. Every hop is another JOIN, and you do not know how many hops
until you have found the path.
The idea
Reach for a graph model when the number of steps is part of the answer — when queries traverse an unknown or variable number of relationships. That single property predicts the fit better than anything about the domain.
The property graph
Two kinds of thing:
- Vertices — the entities. Each has an id, labels (
Person,City), and properties. - Edges — the relationships. Each has a start vertex, an end vertex, a type
(
LIVES_IN,WORKED_AT), and its own properties.
The important structural point is that edges are first-class. An edge has
its own identity and its own attributes, so WORKED_AT can carry from and
to dates. In a relational schema that is a join table; the difference is that
a graph engine stores the edges adjacent to their vertices, so following one is
a pointer hop rather than an index lookup.
Reading a traversal
Cypher (Neo4j’s language, now standardised as openCypher) draws the pattern with ASCII art. Arrows are edges; parentheses are vertices:
MATCH (me:Person {name: 'Ada'})-[:KNOWS*1..4]-(them:Person {name: 'Grace'})
RETURN length(path)
[:KNOWS*1..4] means follow one to four KNOWS edges in either direction.
The variable-length part is the piece that has no clean relational equivalent —
you are asking the engine to search paths, not to join a known number of tables.
A more everyday one:
MATCH (p:Person)-[w:WORKED_AT]->(c:Company)<-[:WORKED_AT]-(other:Person)
WHERE p.name = 'Ada' AND other <> p
RETURN DISTINCT other.name, c.name
Read it as a shape: Ada worked at some company, and someone else also worked at that company. Return them. The query looks like the picture you would draw on a whiteboard, which is most of why people like these languages.
SQL can do this, and it is not fun
Recursive common table expressions handle variable-depth traversal:
WITH RECURSIVE reachable AS (
SELECT knower_id, known_id, 1 AS depth
FROM knows WHERE knower_id = :me
UNION ALL
SELECT k.knower_id, k.known_id, r.depth + 1
FROM knows k
JOIN reachable r ON k.knower_id = r.known_id
WHERE r.depth < 4
)
SELECT DISTINCT known_id, MIN(depth) FROM reachable GROUP BY known_id;
It works. It is standard SQL, it runs in Postgres today, and it needs no new database. It is also considerably harder to read than the Cypher, and it gets harder as the pattern gets richer — add “only edges created after 2020, and stop early if you reach a company vertex” and the recursive CTE becomes something you write comments above.
The honest recommendation
Depth two or three, on data you already store relationally? Write the recursive CTE and keep one database. Traversal is the core of the product, depth is genuinely unbounded, and you are writing new versions of that CTE every week? That is when a graph database earns the operational cost of existing.
Where graphs actually earn their keep
- Fraud detection. The signal is structural: these five accounts share a device, a phone number, and a funding source, through three intermediaries.
- Recommendations from behaviour. People who liked this also liked that, followed several hops out.
- Permissions and org hierarchies. “Can this user access this resource” where access flows through nested groups, roles and delegation of unbounded depth.
- Dependency and impact analysis. If this service degrades, what breaks — transitively?
- Knowledge graphs. Heterogeneous entities with heterogeneous relationships, where the schema genuinely varies per fact.
Check yourself
Which requirement most clearly justifies a graph model over a well-indexed relational schema?
What to take away
The trigger for a graph model is variable traversal depth, not the presence of relationships. When you see it, first check whether a recursive CTE will do — and reach for a dedicated graph engine when traversal is the product rather than a feature of it.