← All articles
Data Engineering

Graph Databases Explained — with Code and Downloadable Sample Data

Vishal Sharma·May 31, 2026·12 min read

A hands-on introduction to property-graph databases: the data model, when to use them over relational, and a complete worked example in Cypher with sample data you can download and load yourself.

Relational databases store rows; graph databases store relationships as first-class citizens. When your most important questions are about how things connect — who knows whom, what leads to what, which path is shortest — a graph model is often dramatically simpler and faster than a pile of join tables. This article explains the model, shows when to reach for it, and walks through a complete, runnable example. Sample data is downloadable at the end.

The property-graph model

A property graph has three ingredients:

  • Nodes — the entities (a Person, a Movie, an Account).
  • Relationships — typed, directed connections between nodes (FRIENDS_WITH, RATED). Relationships can carry properties too.
  • Properties — key/value data on nodes and relationships (name: "Ana", rating: 5).
FRIENDS_WITH RATED AnaRaviMayaSamMovie
A property graph: people (FRIENDS_WITH) and the movies they RATED. Relationships are first-class.

When a graph beats relational

In a relational schema, a "friends of friends who liked this movie" query means self-joining a join table several times. Each extra hop adds another join, and cost grows quickly. In a graph, the same query is a short traversal that follows pointers — the cost is roughly constant per hop regardless of total table size.

120msSQL 3-join260msSQL 5-join22msGraph 3-hop30msGraph 5-hop Illustrative query latency — deep relationship traversal
Illustrative: relational join cost grows with depth; graph traversal cost stays roughly flat per hop.

Reach for a graph when relationships and traversal depth are central: recommendations, fraud rings, knowledge graphs, network/IT topology, identity and access, supply chains, and lineage. Stick with relational for flat, aggregate-heavy reporting workloads.

Worked example: a tiny recommendation graph

We'll model people, their friendships, and the movies they rated, then ask graph-shaped questions. The examples use Cypher, the query language popularized by Neo4j.

1 · Create the data

// People
CREATE (ana:Person {name: 'Ana'}),
       (ravi:Person {name: 'Ravi'}),
       (maya:Person {name: 'Maya'}),
       (sam:Person  {name: 'Sam'});

// Movies
CREATE (m1:Movie {title: 'The Signal', genre: 'SciFi'}),
       (m2:Movie {title: 'Harbor Lights', genre: 'Drama'});

// Friendships (undirected in meaning, stored once)
MATCH (ana:Person {name:'Ana'}), (maya:Person {name:'Maya'})
CREATE (ana)-[:FRIENDS_WITH]->(maya);

// Ratings carry a property
MATCH (maya:Person {name:'Maya'}), (m:Movie {title:'The Signal'})
CREATE (maya)-[:RATED {stars: 5}]->(m);

2 · Ask graph-shaped questions

// Friends-of-friends of Ana (people Ana isn't directly connected to)
MATCH (ana:Person {name:'Ana'})-[:FRIENDS_WITH*2]-(fof)
WHERE fof <> ana
RETURN DISTINCT fof.name AS suggestion;
// Recommend movies that Ana's friends rated 4+ that Ana hasn't rated
MATCH (ana:Person {name:'Ana'})-[:FRIENDS_WITH]-(friend)-[r:RATED]->(m:Movie)
WHERE r.stars >= 4
  AND NOT (ana)-[:RATED]->(m)
RETURN m.title AS movie, avg(r.stars) AS friend_avg, count(*) AS votes
ORDER BY friend_avg DESC, votes DESC;

3 · A sample result

moviefriend_avgvotes
The Signal4.73
Harbor Lights4.01

Notice there are no join tables and no GROUP BY gymnastics across five tables — the traversal expresses the intent directly.

Loading the downloadable sample

Grab the sample files below. nodes.csv and relationships.csv are generic CSVs; load.cypher loads them into Neo4j with LOAD CSV.

// load.cypher (excerpt) — run after placing CSVs in Neo4j's import folder
LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' AS row
CALL {
  WITH row
  FOREACH (_ IN CASE WHEN row.label = 'Person' THEN [1] ELSE [] END |
    MERGE (p:Person {id: row.id}) SET p.name = row.name)
  FOREACH (_ IN CASE WHEN row.label = 'Movie' THEN [1] ELSE [] END |
    MERGE (m:Movie {id: row.id}) SET m.title = row.title, m.genre = row.genre)
} IN TRANSACTIONS;

Practical tips

  • Index your lookup keys (e.g. CREATE INDEX FOR (p:Person) ON (p.id)) — traversal is fast, but the starting-node lookup still needs an index.
  • Model relationships with intent — direction and type carry meaning; don't dump everything into a generic RELATED_TO.
  • Keep properties small — store large blobs elsewhere and reference them.
  • Bound your traversals — an unbounded * hop can explode on dense graphs; cap depth (*1..3).

Takeaway

Graph databases shine when relationships are the data. Model nodes and typed relationships, index your entry points, bound your traversals, and queries that would be painful joins become short, readable paths. Download the sample, load it, and try changing the recommendation query yourself.

Graph DatabaseNeo4jCypherData EngineeringTutorial

Want to see it in action?

Try the live Document Intelligence demo.