Understanding Graph Databases Basics
Understanding Graph Databases Basics
Graph Databases
CHAPTER 3 1
1. What is a Graph Database?
A graph database stores data in a graph, the most generic of data structures, capable of elegantly
representing any kind of data in a highly accessible way. Let’s follow along some graphs, using
themto express graph concepts. We’ll “read” a graph by following arrows around the diagram to
form sentences.
The simplest possible graph is a single Node, a record that has named values referred to as
Properties.A Node could start with a single Property and grow to a few million Properties, though
that can get a little awkward. At some point it makes sense to distribute the data into multiple
nodes, organized withexplicit Relationships.
Labels are a means of grouping the nodes in the graph. They can be used to restrict queries
to subsetsof the graph, as well as enabling optional model constraints and indexing rules.
• Indexes look-up Nodes or Relationships
“An Index —maps from→ Properties —to either→ Nodes or Relationships”
Often, you want to find a specific Node or Relationship according to a Property it has.
Rather than traversing the entire graph, use an Index to perform a look-up, for questions like
“find the Account forusername master-of-graphs.”
Neo4j is a commercially supported open-source graph database. It was designed and built
from the ground-up to be a reliable database, optimized for graph structures instead of
tables. Working with Neo4j, your application gets all the expressiveness of a graph, with
all the dependability you expectout of a database.
CHAPTER 3 2
1.1 . Nodes
The fundamental units that form a graph are nodes and relationships. In Neo4j, both
nodes andrelationships can contain properties.
Nodes are often used to represent entities, but depending on the domain relationships may
be used forthat purpose as well.
Apart from properties and relationships, nodes can also be labeled with zero or more labels.
A Node
can have
Properties
1.2. Relationships
Relationships between nodes are a key part of a graph database. They allow for finding
related [Link] like nodes, relationships can have properties.
A Relationship
uniquely identified by
Nam e
CHAPTER 3 3
A relationship connects two nodes, and is guaranteed to have valid start and end nodes.
relationship
Start node End node
As relationships are always directed, they can be viewed as outgoing or incoming relative to a
node,which is useful when traversing the graph:
Rela%onships are equally well traversed in either direc%on. This means that there is no need
to addduplicate rela<onships in the opposite direc<on (with regard to traversal or
performance).
While relationships always have a direction, you can ignore the direction where it is not useful in
yourapplication.
Note that a node can have relationships to itself as well:
Node loop
The following example shows a simple social network with two relationship types.
Maja Alice
Oscar
blocks
William
CHAPTER 3 4
1.3. Properties
Both nodes and relationships can have properties. Properties are key-value pairs where the key is a
string. Property values can be either a primitive or an array of one primitive type. For example String,
int and int[] values are valid for properties.
A Property
has a has a
Value Key
1.4. Labels
A label is a named graph construct that is used to group nodes into sets; all nodes labeled with the
same label belongs to the same set. Many database queries can work with these sets instead of the
whole graph, making queries easier to write and more efficient. A node may be labeled with any
number of labels, including none, making labels an optional addition to the graph.
A Label
has a groups
Nam e Node
Labels are used when defining contraints and adding indexes for properties.
An example would be a label named User that you label all your nodes representing users with. With
that in place, you can ask Neo4j to perform operations only on your user nodes, such as finding all
users with a given name.
However, you can use labels for much more. For instance, since labels can be added and removed
during runtime, they can be used to mark temporary states for your nodes. You might create an
Offline label for phones that are offline, a Happy label for happy pets, and so on.
2. General Clauses
2.1. Return
In the RETURN part of your query, you define which parts of the pattern you are interested in. It can be nodes,
CHAPTER 3 5
relationships, or properties on these.
nam e = 'A'
happy = 'Yes!'
age = 55
KNOWS BLOCKS
nam e = 'B'
Return nodes
To return a node, list it in the RETURN statement.
Query.
MATCH (n { name: "B" })
RETURN n
Result
n
Node[1]{name:"B"}
1 row
Return relationships
To return a relationship, just include it in the RETURN list.
Query.
MATCH (n { name: "A" })-[r:KNOWS]->(c)
RETURN r
Result
r
:KNOWS[0]{}
1 row
Return property
To return a property, use the dot separator, like this:
Query.
MATCH (n { name: "A" })
RETURN [Link]
CHAPTER 3 6
The value of the property name gets returned.
Result
[Link]
"A"
1 row
This returns the two nodes, the relationship and the path used in the query.
Result
b a r p
Node[1]{name:"B"} Node[0]{name:"A", :KNOWS[0]{} [Node[0]{name:"A",
happy:"Yes!", age:55} happy:"Yes!",
age:55},
:KNOWS[0]{},
Node[1]{name:"B"}]
Node[1]{name:"B"} Node[0]{name:"A", :BLOCKS[1]{} [Node[0]{name:"A",
happy:"Yes!", age:55} happy:"Yes!",
age:55},
:BLOCKS[1]{},
Node[1]{name:"B"}]
2 rows
This example returns the age when the node has that property, or null if the property is not there.
Result
[Link]
55
<null>
2 rows
CHAPTER 3 7
2.3. Other expressions
Any expression can be used as a return item — literals, predicates, properties, functions, and everything
else.
Query.
MATCH (a { name: "A" })
RETURN [Link] > 30, "I'm a literal",(a)-->()
Returns a predicate, a literal and function call with a pattern expression parameter.
Result
[Link] > 30 "I'm a literal" (a)-->()
true "I'm a literal" [[Node[0]{name:"A",
happy:"Yes!", age:55},
:KNOWS[0]
{}, Node[1]{name:"B"}],
[Node[0]
{name:"A", happy:"Yes!",
age:55}, :BLOCKS[1]{}, Node[1]
{name:"B"}]]
1 row
Query.
MATCH (a { name: "A" })-->(b)
RETURN DISTINCT b
Result
b
Node[1]{name:"B"}
1 row
2.5. Order by
To sort the output, use the ORDER BY clause. Note that you can not sort on nodes or relationships, just on
properties on these. ORDER BY relies on comparisons to sort the output.
CHAPTER 3 8
nam e = 'A'
age = 34
length = 170
KNOWS
nam e = 'B'
age = 34
KNOWS
nam e = 'C'
age = 32
length = 185
Result
n
Node[0]{name:"A", age:34, length:170}
Node[1]{name:"B", age:34}
Node[2]{name:"C", age:32, length:185}
3 rows
CHAPTER 3 9
This returns the nodes, sorted first by their age, and then by their name.
Result
n
Node[2]{name:"C", age:32, length:185}
Node[0]{name:"A", age:34, length:170}
Node[1]{name:"B", age:34}
3 rows
Result
n
Node[2]{name:"C", age:32, length:185}
Node[1]{name:"B", age:34}
Node[0]{name:"A", age:34, length:170}
3 rows
2.6. Using
If you do not specify an explicit START clause, Cypher needs to infer where in the graph to start
your query. This is done by looking at the WHERE clause and the MATCH clause and using that
information to find a useful index.
This index might not be the best choice though — sometimes multiple indexes could be used, and
Cypher has picked the wrong one (from a performance point of view).
You can force Cypher to use a specific starting point by using the USING clause. This is called giving
Cypher an index hint.
If your query matches large parts of an index, it might be faster to scan the label and filter out
nodes that do not match. To do this, you can use USING SCAN. It will force Cypher to not use an
index that could have been used, and instead do a label scan.
CHAPTER 3 10
Query.
MATCH (n:Swedish)
USING INDEX n:Swedish(surname)
WHERE [Link] = 'Taylor'
RETURN n
Result
n
Node[3]{name:"Andres", age:36, awesome:true, surname:"Taylor"}
1 row
Result
m
Node[1]{name:"Stefan", surname:"Plantikow"}
1 row
2.7. Where
WHERE is not a clause in it’s own right, rather, it’s part of MATCH.
Swedish
nam e = 'Andres'
age = 36
belt = 'white'
KNOWS KNOWS
Basic usage
CHAPTER 3 11
Query.
MATCH (n)
WHERE [Link] = 'Peter' XOR ([Link] < 30 AND [Link] = "Tobias") OR NOT ([Link] = "Tobias" OR
[Link]="Peter")
RETURN n
Result
n
Node[0]{name:"Tobias", age:25}
Node[1]{name:"Peter", age:34}
Node[2]{name:"Andres", age:36, belt:"white"}
3 rows
Result
n
Node[2]{name:"Andres", age:36, belt:"white"}
1 row
Result
n
Node[0]{name:"Tobias", age:25}
1 row
Property exists
CHAPTER 3 12
To only include nodes/relationships that have a property, use the HAS() function and just write out the
identifier and the property you expect it to have.
Query.
MATCH (n)
WHERE HAS ([Link])
RETURN n
Result
n
Node[2]{name:"Andres", age:36, belt:"white"}
1 row
Cypher is designed to be a humane query language, suitable for both developers and
(importantly, we think) operations professionals. Our guiding goal is to make the simple things
easy, and the complex things possible. Its constructs are based on English prose and neat
iconography which helps to make queries more self-explanatory. We have tried to optimize the
language for reading and not for writing.
Now let’s create a movie and connect it to the Tom Hanks node with an ACTED_IN relationship:
MATCH (actor:Actor)
WHERE [Link] = "Tom Hanks"
CREATE (movie:Movie { title:'Sleepless IN Seattle' })
CREATE (actor)-[:ACTED_IN]->(movie);
Using a WHERE clause in the query above to get the Tom Hanks node does the same thing as the
pattern in the MATCH clause of the previous query.
CHAPTER 3 13
This is how our graph looks now:
Actor
ACTED_IN
Movie
t it le = 'Sleepless in Seattle'
We can do more of the work in a single clause. CREATE UNIQUE will make sure we don’t create
duplicate patterns. Using this: [r:ACTED_IN] lets us return the relationship.
MATCH (actor:Actor { name: "Tom Hanks" })
CREATE UNIQUE (actor)-[r:ACTED_IN]->(movie:Movie { title:"Forrest Gump" })
RETURN r;
The labels Actor and Movie help us organize the graph. Let’s list all Movie nodes:
MATCH (movie:Movie)
RETURN movie AS `All Movies`;
All Movies
Node[1]{title:"Sleepless in Seattle"}
Node[2]{title:"Forrest Gump"}
2 rows
CHAPTER 3 14
4.
Outgoing relationships
When the direction of a relationship is interesting, it is shown by using --> or <--, like this:
Query.
MATCH (martin { name:'Martin Sheen' })-->(movie)
RETURN [Link]
Result
[Link]
"Wall Street"
"The American President"
2 rows
Result
r
:ACTED_IN[1]{}
:ACTED_IN[3]{}
2 rows
CHAPTER 3 15
When you know the relationship type you want to match on, you can specify it by using a colon
together with the relationship type.
Query.
Result
actor
Node[1]{name:"Charlie Sheen"}
Node[2]{name:"Martin Sheen"}
Node[6]{name:"Michael Douglas"}
3 rows
Result
person
Node[0]{name:"Oliver Stone"}
Node[1]{name:"Charlie Sheen"}
Node[2]{name:"Martin Sheen"}
Node[6]{name:"Michael Douglas"}
4 rows
Result
CHAPTER 3 16
r
:ACTED_IN[0]{}
:ACTED_IN[1]{}
:ACTED_IN[2]{}
3 rows
Multiple relationships
Relationships can be expressed by using multiple statements in the form of ()--(), or they can be
strung together, like this:
Query.
MATCH (charlie { name:'Charlie Sheen' })-[:ACTED_IN]->(movie)<-[:DIRECTED]->(director)
RETURN charlie,movie,director
Result
charlie movie director
Node[1]{name:"Charlie Sheen"} Node[4]{name:"WallStreet", Node[0]{name:"Oliver Stone"}
title:"Wall Street"}
1 row
Query.
MATCH (a)-[r]-(b)
WHERE id(r)= 0
RETURN a,b
This returns the two connected nodes, once as the start node, and once as the end node.
Result
a b
Node[1]{name:"Charlie Sheen"} Node[4]{name:"WallStreet", title:"Wall Street"}
Node[4]{name:"WallStreet", title:"Wall Street"} Node[1]{name:"Charlie Sheen"}
2 rows
CHAPTER 3 17
has a role attribute.
We’ll go with three movies and three actors:
CREATE (matrix1:Movie { title : 'The Matrix', year : '1999-03-31' })
CREATE (matrix2:Movie { title : 'The Matrix Reloaded', year : '2003-05-07' })
CREATE (matrix3:Movie { title : 'The Matrix Revolutions', year : '2003-10-27' })
CREATE (keanu:Actor { name:'Keanu Reeves' })
CREATE (laurence:Actor { name:'Laurence Fishburne' })
CREATE (carrieanne:Actor { name:'Carrie-Anne Moss' })
CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix1)
CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix2)
CREATE (keanu)-[:ACTS_IN { role : 'Neo' }]->(matrix3)
CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix1)
CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix2)
CREATE (laurence)-[:ACTS_IN { role : 'Morpheus' }]->(matrix3)
CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix1)
CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix2)
CREATE (carrieanne)-[:ACTS_IN { role : 'Trinity' }]->(matrix3)
[Link] [Link]
"The Matrix" "1999-03-31"
1 row
CHAPTER 3 18
Return just the name, and order them by name:
MATCH (actor:Actor)
RETURN [Link]
ORDER BY [Link];
Here’s some exploratory queries for unknown datasets. Don’t do this on live produc%on
databases!
Count nodes:
MATCH (n)
RETURN count(*);
type(r) count(*)
"ACTS_IN" 9
1 row
from -> to
Node[3]{name:"Keanu Reeves"} :ACTS_IN[0]{role:"Neo"} Node[0]{title:"The
Matrix", year:"1999-03-
31"}
Node[3]{name:"Keanu Reeves"} :ACTS_IN[1]{role:"Neo"} Node[1]{title:"The Matrix
Reloaded", year:"2003-05-07"}
Node[3]{name:"Keanu Reeves"} :ACTS_IN[2]{role:"Neo"} Node[2]{title:"The Matrix
Revolutions", year:"2003-10-
27"}
Node[4]{name:"Laurence :ACTS_IN[3]{role:"Morpheus"} Node[0]{title:"The
Fishburne"} Matrix", year:"1999-03-
31"}
Node[4]{name:"Laurence :ACTS_IN[4]{role:"Morpheus"} Node[1]{title:"The Matrix
Fishburne"} Reloaded", year:"2003-05-07"}
CHAPTER 3 19
Node[4]{name:"Laurence :ACTS_IN[5]{role:"Morpheus"} Node[2]{title:"The Matrix
Fishburne"} Revolutions", year:"2003-10-
27"}
Node[5]{name:"Carrie-Anne :ACTS_IN[6]{role:"Trinity"} Node[0]{title:"The
Moss"} Matrix", year:"1999-03-
31"}
Node[5]{name:"Carrie-Anne :ACTS_IN[7]{role:"Trinity"} Node[1]{title:"The Matrix
Moss"} Reloaded", year:"2003-05-07"}
Node[5]{name:"Carrie-Anne :ACTS_IN[8]{role:"Trinity"} Node[2]{title:"The Matrix
Moss"} Revolutions", year:"2003-10-
27"}
9 rows
me
Node[6]{name:"Me"}
1 row
Nodes created: 1
Properties set: 1
Labels added: 1
CHAPTER 3 20
Let’s check if the node is there:
MATCH (me:User { name: "Me" })
RETURN [Link];
We need a friend!
CREATE (friend:User { name: "A Friend" })
RETURN friend;
Add our friendship idempotently, so we can re-run the query without adding it several
times. We return the relationship to check that it has not been created several times.
MATCH (me:User { name: "Me" }),(friend:User { name: "A Friend" })
CREATE UNIQUE (me)-[friendship:FRIEND]->(friend)
RETURN friendship;
You can rerun the query, see that it doesn’t change anything the second
time! Let’s update our friendship with a since property:
MATCH (me:User { name: "Me" })-[friendship:FRIEND]->(friend:User { name: "A Friend" })
SET [Link]='forever'
RETURN friendship;
Let’s pretend us being our friend and wanting to see which movies our friends have rated.
MATCH (me:User { name: "A Friend" })-[:FRIEND]-(friend)-[rating:RATED]->(movie)
RETURN [Link], avg([Link]) AS stars, collect([Link]) AS comments, count(*);
That’s too little data, let’s add some more friends and friendships.
MATCH (me:User { name: "Me" })
FOREACH (i IN range(1,10)| CREATE (friend:User { name: "Friend " + i }),(me)-[:FRIEND]->(friend));
CHAPTER 3 21
friendship [Link]
"FRIEND" "A Friend"
"FRIEND" "Friend 1"
"FRIEND" "Friend 2"
"FRIEND" "Friend 3"
"FRIEND" "Friend 4"
"FRIEND" "Friend 5"
"FRIEND" "Friend 6"
"FRIEND" "Friend 7"
"FRIEND" "Friend 8"
"FRIEND" "Friend 9"
"FRIEND" "Friend 10"
10 rows
Note that adding the unique constraint will add an index on that property, so we won’t do that
separately. If we drop the constraint, we will have to add an index instead, as needed.
In this case we want an index to speed up finding actors by name in the database:
CREATE INDEX ON :Actor(name)
Indexes can be added at any time. Constraints can be added after a label is already in use, but
that requires that the existing data complies with the constraints. Note that it will take some time
for an index to come online when there’s existing data.
Now, let’s add some data.
CREATE (actor:Actor { name:"Tom Hanks" }),(movie:Movie { title:'Sleepless IN Seattle' }),
(actor)-[:ACTED_IN]->(movie);
Normally you don’t specify indexes when querying for data. They will be used automatically.
This means we can simply look up the Tom Hanks node, and the index will kick in behind the
scenes to boost performance.
MATCH (actor:Actor { name: "Tom Hanks" })
RETURN actor;
Now let’s say we want to add another label for a node. Here’s how to do that:
CHAPTER 3 22
MATCH (actor:Actor { name: "Tom Hanks" })
SET actor :American;
3.6. Aggregations
To calculate aggregated data, Cypher offers aggregation, much like SQL’s GROUP BY.
Aggregate functions take multiple input values and calculate an aggregated value from them.
Examples are avg that calculates the average of multiple numeric values, or min that finds the
smallest numeric value in a set of values.
Aggregation can be done over all the matching sub graphs, or it can be further divided by introducing
key values. These are non-aggregate expressions, that are used to group the values going into the
aggregate functions.
So, if the return statement looks something like this:
RETURN n, count(*)
We have two return expressions — n, and count(*). The first, n, is no aggregate function, and so it
will be the grouping key. The latter, count(*) is an aggregate expression. So the matching
subgraphs will be divided into different buckets, depending on the grouping key. The aggregate
function will then run on these buckets, calculating the aggregate values.
If you want to use aggregations to sort your result set, the aggregation must be included in the RETURN
to be used in your ORDER BY.
The last piece of the puzzle is the DISTINCT keyword. It is used to make all values unique before
running them through an aggregate function.
An example might be helpful. In this case, we are running the query against the following data:
CHAPTER 3 23
Person
nam e = 'A'
property = 13
Person Person
Person
nam e = 'B' nam e = 'C'
nam e = 'D'
property = 33 property = 44
eyes = 'brown'
eyes = 'blue' eyes = 'blue'
KNOWS KNOWS
Person
nam e = 'D'
Query.
MATCH (me:Person)-->(friend:Person)-->(friend_of_friend:Person)
WHERE [Link] = 'A'
RETURN count(DISTINCT friend_of_friend), count(friend_of_friend)
In this example we are trying to find all our friends of friends, and count them. The first
aggregate funcNon, count(DISTINCT friend_of_friend), will only see a friend_of_friend once —
DISTINCT removes the duplicates. The laXer aggregate funcNon, count(friend_of_friend), might
very well see the same friend_of_friend mulNple Nmes. In this case, both B and C know D and
thus D will get counted twice, when not using DISTINCT.
Result
count(distinct friend_of_friend) count(friend_of_friend)
1 2
1 row
The following examples are assuming the example graph structure below.
CHAPTER 3 24
Person
nam e = 'A'
property = 13
Person Person
Person
nam e = 'B' nam e = 'C'
nam e = 'D'
property = 33 property = 44
eyes = 'brown'
eyes = 'blue' eyes = 'blue'
COUNT
COUNT is used to count the number of rows. COUNT can be used in two forms — COUNT(*) which
just counts the number of matching rows, and COUNT(<identifier>), which counts the number
of non-NULL values in <identifier>.
Count nodes
To count the number of nodes, for example the number of nodes connected to one node, you can use
count(*).
Query.
MATCH (n { name: 'A' })-->(x)
RETURN n, count(*)
This returns the start node and the count of related nodes.
Result
n count(*)
Node[1]{name:"A", property:13} 3
1 row
The relationship types and their group count is returned by the query.
Result
type(r) count(*)
"KNOWS" 3
1 row
CHAPTER 3 25
Count entities
Instead of counting the number of results with count(*), it might be more expressive to include the
name of the identifier you care about.
Query.
MATCH (n { name: 'A' })-->(x)
RETURN count(x)
The example query returns the number of connected nodes from the start node.
Result
count(x)
3
1 row
The count of related nodes with the property property set is returned by the query.
Result
count([Link])
3
1 row
sum
The sum aggregation function simply sums all the numeric values it encounters. NULLs are silently
dropped.
Query.
MATCH (n:Person)
RETURN sum([Link])
This returns the sum of all the values in the property property.
Result
sum([Link])
90
1 row
avg
avg calculates the average of a numeric column.
Query.
CHAPTER 3 26
MATCH (n:Person)
RETURN avg([Link])
The average of all the values in the property property is returned by the example query.
Result
avg([Link])
30. 0
1 row
max
max find the largest value in a numeric column.
Query.
MATCH (n:Person)
RETURN max([Link])
min
min takes a numeric property as input, and returns the smallest value in that column.
Query.
MATCH (n:Person)
RETURN min([Link])
This returns the smallest of all the values in the property property.
Result
min([Link])
13
1 row
collect
collect collects all the values into a list. It will ignore NULLs.
Query.
MATCH (n:Person)
RETURN collect([Link])
Result
CHAPTER 3 27
collect([Link])
[13, 33, 44]
1 row
DISTINCT
All aggregation functions also take the DISTINCT modifier, which removes duplicates from the
values. So, to count the number of unique eye colors from nodes related to a, this query can be
used:
Query.
MATCH (a:Person { name: 'A' })-->(b)
RETURN count(DISTINCT [Link])
Result
count(distinct [Link])
2
1 row
nam e = 'Joe'
knows
knows knows
knows knows
CHAPTER 3 28
To find out the friends of Joe’s friends that are not already his friends, the query looks like this:
Query.
MATCH (joe { name: 'Joe' })-[:knows*2..2]-(friend_of_friend)
WHERE NOT (joe)-[:knows]-(friend_of_friend)
RETURN friend_of_friend.name, COUNT(*)
ORDER BY COUNT(*) DESC , friend_of_friend.name
This returns a list of friends-of-friends ordered by the number of connections to them, and
secondly by their name.
Result
friend_of_friend.name COUNT(*)
"Ian" 2
"Derrick" 1
"Jill" 1
3 rows
To find out the possible new friends based on them liking similar things as the asking person,
use a query like this:
Query.
MATCH (me { name: 'Joe' })-[:favorite]->(stuff)<-[:favorite]-(person)
WHERE NOT (me)-[:friend]-(person)
RETURN [Link], count(stuff)
ORDER BY count(stuff) DESC
The list of possible friends ranked by them liking similar stuff that are not yet friends is returned.
Result
[Link] count(stuff)
"Derrick" 2
"Jill" 1
CHAPTER 3 29
2 rows
In this scenario, the problem is to determine mutual friends and groups, if any, between persons. If
no mutual groups or friends are found, there should be a 0 returned.
Query.
MATCH (me { name: 'Joe' }),(other)
WHERE [Link] IN ['Jill', 'Bob']
OPTIONAL MATCH pGroups=(me)-[:member_of_group]->(mg)<-[:member_of_group]-(other)
OPTIONAL MATCH pMutualFriends=(me)-[:knows]->(mf)<-[:knows]-(other)
RETURN [Link] AS name, count(DISTINCT pGroups) AS mutualGroups,
count(DISTINCT pMutualFriends) AS mutualFriends
ORDER BY mutualFriends DESC
The question we are asking is — how many unique paths are there between me and Jill, the
paths being common group memberships, and common friends. If the paths are mandatory, no
results will be returned if me and Bob lack any common friends, and we don’t want that. To
make a path optional, you have to make at least one of it’s relationships optional. That makes
the whole path optional.
Result
name mutualGroups mutualFriends
"Jill" 1 1
"Bob" 1 0
2 rows
CHAPTER 3 30
4.4. Example of TV Shows
This example show how TV Shows with Seasons, Episodes, Characters, Actors, Users and
Reviews can be modeled in a graph database.
Data Model
Let’s start out with an entity-relationship model of the domain at hand:
TV Show
has
Season
has
Review Character
Sample Data
Let’s create some data and see how the domain plays out in practice:
CHAPTER 3 31
CREATE (himym:TVShow { name: "How I Met Your Mother"
TVShow
HAS_SEASON
Season
HAS_EPISODE
Character
t it le = 'Meet Me At The Bar In 15 Minutes & Suit Up'
nam e = 'Ted Mosby' content = 'It was awesom e'
Note that even though we could have modeled the reviews as relationships with title and content
properties on them, we made them nodes instead. We gain a lot of flexibility in this way, for
example if we want to connect comments to each review.
Now let’s add more data:
CHAPTER 3 32
MATCH (himym:TVShow { name: "How I Met Your Mother" }),(himym_s1:Season),
(himym_s1_e1:Episode { name: "Pilot" }),
(himym)-[:HAS_SEASON]->(himym_s1)-[:HAS_EPISODE]->(himym_s1_e1)
CREATE (marshall:Character { name: "Marshall Eriksen" })
CREATE (robin:Character { name: "Robin Scherbatsky" })
CREATE (barney:Character { name: "Barney Stinson" })
CREATE (lily:Character { name: "Lily Aldrin" })
CREATE (jasonSegel:Actor { name: "Jason Segel" })
CREATE (cobieSmulders:Actor { name: "Cobie Smulders" })
CREATE (neilPatrickHarris:Actor { name: "Neil Patrick Harris" })
CREATE (alysonHannigan:Actor { name: "Alyson Hannigan" })
CREATE UNIQUE (jasonSegel)-[:PLAYED_CHARACTER]->(marshall)
CREATE UNIQUE (cobieSmulders)-[:PLAYED_CHARACTER]->(robin)
CREATE UNIQUE (neilPatrickHarris)-[:PLAYED_CHARACTER]->(barney)
CREATE UNIQUE (alysonHannigan)-[:PLAYED_CHARACTER]->(lily)
CREATE UNIQUE (himym_s1_e1)-[:FEATURED_CHARACTER]->(marshall)
CREATE UNIQUE (himym_s1_e1)-[:FEATURED_CHARACTER]->(robin)
CREATE UNIQUE (himym_s1_e1)-[:FEATURED_CHARACTER]->(barney)
CREATE UNIQUE (himym_s1_e1)-[:FEATURED_CHARACTER]->(lily)
CREATE (himym_s1_e1_review2 { title: "What a great pilot for a show :)",
content: "The humour is great." })
CREATE (atlasredux:User { name: "atlasredux" })
CREATE (atlasredux)-[:WROTE_REVIEW]->(himym_s1_e1_review2)<-[:HAS_REVIEW]-(himym_s1_e1)
[Link] [Link]
"HIMYM Season 1" "Pilot"
1 row
We could also grab the reviews if there are any by slightly tweaking the query:
MATCH (tvShow:TVShow)-[:HAS_SEASON]->(season)-[:HAS_EPISODE]->(episode)
WHERE [Link] = "How I Met Your Mother"
WITH season, episode
OPTIONAL MATCH (episode)-[:HAS_REVIEW]->(review)
RETURN [Link], [Link], review
CHAPTER 3 33
Now let’s list the characters featured in a show. Note that in this query we only put identifiers on
the nodes we actually use later on. The other nodes of the path pattern are designated by ().
MATCH (tvShow:TVShow)-[:HAS_SEASON]->()-[:HAS_EPISODE]->()-[:FEATURED_CHARACTER]->(character)
WHERE [Link] = "How I Met Your
Mother" RETURN DISTINCT [Link]
[Link]
"Ted Mosby"
"Marshall Eriksen"
"Robin Scherbatsky"
"Barney Stinson"
"Lily Aldrin"
5 rows
[Link]
"Josh Radnor"
"Jason Segel"
"Cobie Smulders"
"Neil Patrick Harris"
"Alyson Hannigan"
5 rows
And now we’ll create a query to find the episodes that he has appeared in:
MATCH (actor:Actor)-[:PLAYED_CHARACTER]->(character)<-[:FEATURED_CHARACTER]-(episode)
WHERE [Link] = "Josh Radnor"
RETURN [Link] AS Episode, [Link] AS Character
CHAPTER 3 34
Episode Character
"Pilot" "Ted Mosby"
"Peter's Progress" "Keith"
2 rows
Now let’s go for a similar query, but add the season and show to it as well.
MATCH (actor:Actor)-[:PLAYED_CHARACTER]->(character)<-[:FEATURED_CHARACTER]-(episode),
(episode)<-[:HAS_EPISODE]-(season)<-[:HAS_SEASON]-(tvshow)
WHERE [Link] = "Josh Radnor"
RETURN [Link] AS Show, [Link] AS Season, [Link] AS Episode,
[Link] AS Character
CHAPTER 3 35
CHAPTER 3 36