Subchapter 67.6
references/data-modeling.mdMarkdown5 KBView on GitHub
Graph data modeling is fundamentally different from relational modeling. The most common mistake agents make is modeling a graph like a relational table — normalizing everything into entities with foreign keys.
In a graph database, the query pattern drives the model. Ask first: “What relationships will I traverse?” Then model edges to support those traversals.
Vertex (node): An entity with a label and properties
Example: vertex with label "Person", properties {name: "Alice", age: 30}
Edge: A directed relationship between two vertices, with a label and properties
Example: edge with label "PURCHASED", properties {date: "2024-01-15", amount: 99.99}
from vertex "Person:Alice" to vertex "Product:Widget"Relational model:
customers(id, name) — orders(id, customer_id, date) — products(id, name)Graph model:
(Customer {name:"Alice"}) -[PLACED {date:"2024-01-15"}]-> (Order {id:"O1"})
(Order {id:"O1"}) -[CONTAINS {qty:2}]-> (Product {name:"Widget"})The graph model enables: “Find all products bought by Alice’s friends” in a single traversal. The relational model requires multiple joins.
Add temporal properties to edges rather than creating time-based vertices:
// Good: date on edge
g.addE('PURCHASED').from(customer).to(product)
.property('date', '2024-01-15')
.property('amount', 99.99)
// Avoid: intermediate time vertex (adds traversal hops without benefit)
// customer -[ON]-> Date -[PURCHASED]-> productModel shared identifiers (email, phone, device) as vertices, entities as edges to those identifiers:
(Account:A1) -[USES]-> (Email:alice@example.com) <-[USES]- (Account:A2)
(Account:A1) -[USES]-> (Device:iPhone-XYZ) <-[USES]- (Account:A3)Query: find all accounts sharing identifiers with a flagged account:
g.V().has('Account', 'id', 'A1')
.out('USES').in('USES')
.dedup()
.values('id')(Category:Electronics) -[PARENT_OF]-> (Category:Phones)
-[PARENT_OF]-> (Category:Laptops)
(Category:Phones) -[PARENT_OF]-> (Category:Smartphones)Query full path from root:
g.V().has('Category', 'name', 'Smartphones')
.repeat(__.in('PARENT_OF'))
.until(__.inE('PARENT_OF').count().is(0))
.path()// Bad: one "USA" vertex connected to every US customer
(Country:USA) <-[LIVES_IN]- (all 10M US customers)Supernodes degrade traversal performance. The vertex becomes a bottleneck.
Fix: denormalize the property onto the customer vertex instead.
// Good: filter by property, avoid the hub vertex
g.V().hasLabel('Customer').has('country', 'USA').limit(100)If the hub is unavoidable (e.g., social media influencer with 1M followers),
partition traversals and use sample() or limit() early.
// Bad: storing connections as a list property
vertex: {friends: ["Bob", "Carol", "Dave"]}This forces application-side joins and makes traversal impossible in Neptune. Model connections as edges.
Every extra hop adds latency. Only create intermediate vertices when the intermediate entity has its own properties or relationships you’ll query.
RDF models data as triples: subject predicate object
:Alice rdf:type :Person .
:Alice :name "Alice" .
:Alice :knows :Bob .
:Alice :worksFor :AcmeCorp .Use RDF when:
For application backends, property graph is almost always simpler.
Neptune is schema-free. There is no DDL. Vertex labels, edge labels, and property keys are defined by your first write. This is flexible but requires discipline:
querying (query the model), use-cases (concrete patterns)