Graph Data Modeling: When Relationships Are the Data
Some questions are easy to ask and brutally hard to answer in SQL. โWhich of my customers know each other?โ โIs this new account connected โ through any chain of shared devices, addresses, or payment methods โ to a known fraudster?โ โWhatโs the shortest chain of introductions from me to this executive?โ Each of these is a question about paths through relationships, and relational databases, despite the name, make paths expensive: every hop is another self-join, and query cost explodes with depth.
Graph databases invert the trade. Relationships are stored as first-class, directly-traversable objects, so following a connection costs the same whether itโs hop one or hop seven. This post covers how to model for them: the property graph model, the design process, patterns and pitfalls, and an honest account of when not to use a graph.
The Property Graph Model in Two Minutes
The dominant model (Neo4j, Amazon Neptune, Memgraph) has four elements:
- Nodes โ the entities:
(:Person),(:Account),(:Device). - Labels โ a nodeโs type(s); a node can carry several.
- Relationships โ directed, named connections between nodes:
(:Person)-[:OWNS]->(:Account). Every relationship has a type and a direction (though you can traverse both ways). - Properties โ key-value data on both nodes and relationships: a
:TRANSFERREDrelationship can carryamountandtimestamp.
That last point is the modelโs quiet superpower. In relational design, a relationship with attributes forces a junction table; in a graph, the relationship simply carries its data:
(alice:Person {name:"Alice"})-[:TRANSFERRED {amount: 5000, at: "2026-06-12"}]->(bob:Person {name:"Bob"})When a Graph Beats a Relational Model
The honest test is not โis my data connected?โ โ all data is connected โ but โdo my queries traverse a variable or deep number of hops?โ Compare the question โwho are Aliceโs friends-of-friends-of-friends?โ in both worlds.
SQL, three self-joins deep and growing per level:
SELECT DISTINCT f3.friend_idFROM friendships f1JOIN friendships f2 ON f2.person_id = f1.friend_idJOIN friendships f3 ON f3.person_id = f2.friend_idWHERE f1.person_id = 42;Cypher (the graph query language), where depth is a parameter, not a rewrite:
MATCH (a:Person {id: 42})-[:FRIENDS_WITH*1..3]-(fof)RETURN DISTINCT fof;The graph engine follows stored pointers from node to node โ no index lookups per hop, no join planning. Workloads where this dominates: fraud rings, social networks, recommendation engines (โcustomers who bought what you bought also boughtโฆโ), knowledge graphs, network/IT topology, supply-chain tracing, and access-control resolution (โdoes this user, through any role chain, have permission X?โ).
If your queries are aggregations over millions of uniform rows (โtotal sales by regionโ), a graph is the wrong tool โ columnar warehouses crush graphs at scan-and-aggregate work.
The Modeling Process: Nouns, Verbs, Questions
Graph modeling is closer to natural language than any other paradigm, which makes whiteboard sessions with domain experts remarkably productive:
- Write the questions first โ as sentences. โFind accounts sharing a device with a flagged account.โ
- Nouns become nodes: Account, Device, Person.
- Verbs become relationships: USES, OWNS, FLAGGED_AS.
- Check each question is a path through your sketch. If answering requires assembling data that isnโt connected, add the missing relationship.
The most important design instinct: things youโll traverse through must be nodes, not properties. Suppose you model a personโs email as a property: (:Person {email: "x@y.com"}). Now โfind people sharing an emailโ is a full scan comparing property values โ the graph gives you nothing. Model the email as a node โ (:Person)-[:HAS_EMAIL]->(:Email {addr}) โ and shared emails become shared nodes: two OWNS arrows converging on one point, traversable in constant time. The rule generalizes: attributes are for filtering, nodes are for connecting. Whenever a value can link entities (emails, devices, addresses, skills, tags), promote it to a node.
A Worked Example: Fraud Ring Detection
Fraud teams discovered graphs early because fraud is structurally invisible in rows. Individually, each account looks fine; the fraud is the shape โ many accounts quietly sharing infrastructure.
Model: (:Account)-[:USES]->(:Device), (:Account)-[:PAYS_WITH]->(:Card), (:Account)-[:REGISTERED_AT]->(:Address). The killer query โ โfind accounts within three hops of a flagged account through any shared identifierโ โ is a few lines of Cypher and runs in milliseconds, because it just walks pointers outward from the flagged node. The relational equivalent is a UNION of self-joins per identifier type per depth, and most teams simply never build it.
Patterns and Pitfalls
Qualify your relationships. RATED {stars: 4} beats separate LIKED/DISLIKED types when the value matters; but prefer specific relationship types (ACTED_IN, DIRECTED) over one generic RELATED_TO with a type property โ traversal by relationship type is the engineโs fastest filter.
Time on relationships. Facts change: employment, ownership, friendship. Put from/to properties on relationships to keep history traversable: -[:WORKED_AT {from: 2019, to: 2023}]->. โWho worked together at Acme in 2021?โ stays a path query.
Beware supernodes. A node with millions of relationships โ a (:Country {name:"India"}) node every user connects to, or a celebrity in a social graph โ turns every traversal that touches it into a scan of its relationship list. Mitigations: donโt model low-selectivity categories as nodes (make country a property or an index instead โ connecting through it tells you almost nothing anyway), and for legitimate hubs, filter by relationship type/direction early or shard the hub.
Donโt graph your whole enterprise. The successful pattern is a graph for the relationship-shaped subdomain (fraud, recommendations, the knowledge graph), fed from systems of record that remain relational or document-based. Graphs earn their keep on traversal queries; theyโre a poor system of record for bulk transactional data.
Getting Started Checklist
- Write your ten questions as sentences; confirm at least half involve variable-depth or multi-type traversal. If not, stop โ use SQL.
- Sketch nodes (nouns) and relationships (verbs); walk every question as a path on the sketch.
- Promote any โconnectingโ attribute (email, device, tag) to a node.
- Add time properties to relationships that change.
- Load a realistic sample and profile your worst query for supernodes before committing.
Graph modeling rewards a specific kind of thinking: instead of asking โwhat are my entities?โ, you ask โwhat do I want to walk between?โ When the questions are about connection โ who touches whom, what leads to what, how close is A to B โ no other model comes close. When they arenโt, no amount of graph enthusiasm will beat a well-indexed join. Knowing which situation youโre in is the modeling skill this post was really about.