Cypher: nodes, relationships, patterns and paths

After this lesson you will write a Cypher pattern as a drawing, create, match and delete with it, count paths and people without mixing them up, and say which algorithm Neo4j runs for a shortest path — and when it can miss one.

Johan wants to learn to surf. On the exercise slides you ask Neo4j for the friends of his friends who surf, and you count them twice. COUNT(surfer) answers 3. COUNT(DISTINCT surfer) answers 1. The graph holds exactly one surfer, Allison, and both numbers are right.

Later in the same session two shortest-path queries differ by two words, WITH p. One is labelled "Always returns a result, if exists", the other "Not guaranteed to return a result, even if it exists" (p. 1373). Keep both puzzles open. Both come from reading a pattern literally: Neo4j looks for every place where your drawing fits the graph, and what you attach to the drawing decides how it searches.

Cypher is ASCII art

The session opens with a definition worth an exam answer: Neo4j "is a graph database management system implemented in Java. It is A.C.I.D. compliant. It has its own query language, named Cypher" (p. 1332). Cypher "focuses on the clarity of expressing what to retrieve from a graph, not on how to retrieve it" (p. 1383): it is declarative, like SQL. The property graph itself is the subject of lesson 10; here you learn to write it down.

The slide that introduces Cypher is a drawing: a circle "You", an arrow ARE_LEARNING, a circle "Cypher". Cypher types that drawing on the keyboard: round brackets for a node, square brackets inside an arrow for a relationship (Fig. 1).

Emil Johan :Person :Person KNOWS since: 2001 a drawing

What you draw

(ee:Person {name: "Emil"}) node: round brackets -[:KNOWS {since: 2001}]-> relationship: square brackets in an arrow (js:Person {name: "Johan"}) colon: label · braces: properties

What you type

Fig. 1. One fact of the session's first dataset, Emil knows Johan since 2001, drawn and typed (the variables ee and js are the ones of pp. 1334 and 1346). In a query the three lines are written on one line.

A node pattern holds a variable, zero or more labels after colons, and properties in curly braces (p. 1336); every part is optional. The variable is a name that lives inside one query; a label is a type tag, and a node may carry several; the property map is a set of equality conditions in MATCH and of values to store in CREATE.

The pattern vocabulary of pp. 1336 and 1341, with the slides' own readings (the last row is from p. 1359).
PatternThe slide's reading
()"Empty node": any node
(n)"A node identified as “n”"
(n:Person)"The node “n” labeled as “Person”"
(n:Person:Manager)"The node “n” labeled as “Person” and “Manager”"
(n:Person {name: "Mark"})"… with the property “name” equal to “Mark”"
[:KNOWS]"Relationship labeled as “KNOWS”"
[k:KNOWS]"Relationship “KNOWS” identified as “k”"
[k:KNOWS {since:2007}]"Relationship “KNOWS” with a parameter “since”" (red pen below)
[relatedTo]"Only variable name (any relationship)"

A relationship pattern has the same three parts, but a relationship has exactly one type. The colon is the whole difference between a name and a filter: [relatedTo] is a variable that matches a relationship of any type, [:ACTED_IN] is a type filter without a name, and [r:ACTED_IN] is both. The slides drop this colon once themselves, on p. 1365; the red pen is in lesson 12.

No colon, no filter: [ACTED_IN] is a variable that matches any relationship; [:ACTED_IN] matches ACTED_IN only.

The arrow head is a filter too. -[:MANAGES]-> and <-[:MANAGES]- fix the direction, and one relationship can be written from either end: (a:Employee)<-[:MANAGES]-(b:Manager) is "a is managed by b" (p. 1344). Leave the head out, -[:KNOWS]-, and the relationship may point either way. Chain them and you have a path pattern: (p)-[:KNOWS]-(friend)-[:KNOWS]-(foaf) is the slide's "Friend-of-a-friend relationship" — in SQL, a Knows table joined with itself.

Writing the graph: CREATE, MERGE, SET, DELETE

CREATE "allows the creation of nodes and relationships" (p. 1334); the slide labels the parts "Variable : Node Label", "Node Properties", "Node - Identified by the parenthesis".

// creates 1 node with the label Person and 3 properties
CREATE (ee:Person { name: "Emil", from: "Sweden", klout: 99 })

One CREATE can write several nodes, separated by commas, and a property may hold a list (p. 1335):

// creates 3 Person nodes; Francis's "from" is a list
CREATE (ee:Person { name: "Emil", from: "Sweden", klout: 99 }),
  (a:Person { name: "Richard", from: "UK", klout: 80 }),
  (j:Person { name: "Francis", from: ["Sweden","UK"], klout: 75 })

The same slide writes a node with two labels and one with none, and returns what it has just created. Double and single quotes both delimit strings; a list holds values of one type, and a property cannot hold a nested map.

// 2 nodes: one :Person:Fisherman, one with no label
CREATE (ee:Person:Fisherman), (a)
// returns 1 row: "Andy"
CREATE (a {name: 'Andy'}) RETURN a.name

A relationship needs its two end nodes first. Page 1339 creates Mark and Mike, finds them, and draws the arrow:

// creates 2 Person nodes, Mark and Mike
CREATE (e:Person {name: "Mark"}), (a:Person {name: "Mike"})
// returns 1 row: "KNOWS"
MATCH
  (a:Person),
  (b:Person)
WHERE a.name = 'Mike' AND b.name = 'Mark'
CREATE (a)-[r:KNOWS]->(b)
RETURN type(r)

Neo4j adds two rules the slide leaves implicit: in CREATE a relationship needs a direction ("Only directed relationships are supported in CREATE") and exactly one type ("Exactly one relationship type must be specified for CREATE"). In MATCH both may be left out.

Now run the same MATCH with a second CREATE (p. 1340):

// returns "KNOWS"; Mike now has two KNOWS and one BEFRIEND to Mark
MATCH
  (a:Person),
  (b:Person)
WHERE a.name = 'Mike' AND b.name = 'Mark'
CREATE (a)-[r:KNOWS {since: 2015}]->(b), (a)-[h:BEFRIEND]->(b)
RETURN type(r)

The slide insists: "N.B. The two KNOWS relationships we created are different even though they have the same name". CREATE never looks before it writes. The create-if-absent clause is MERGE, which lecture 06 shows "ID-based" on p. 270: MERGE (player:Player {id: row.playerUri}) finds the player with that id or creates it, and its ON CREATE SET part runs only on creation.

// creates a KNOWS from Mike to Mark only if none exists;
// a second run finds it and writes nothing
MATCH (a:Person {name: 'Mike'}), (b:Person {name: 'Mark'})
MERGE (a)-[:KNOWS]->(b)
Mike Mark KNOWS KNOWS two copies

Before: CREATE, run twice

Mike Mark KNOWS still one

After: MERGE, run twice

Fig. 2. The same MATCH of Mike and Mark, followed by CREATE (a)-[:KNOWS]->(b) or by MERGE (a)-[:KNOWS]->(b), each run twice. CREATE writes a new relationship every time; MERGE first looks for the whole pattern and writes only when it is missing.

"MERGE checks the key." No: it matches the whole pattern. If Mark has no age, MERGE (p:Person {name: 'Mark', age: 30}) creates a second Mark. Merge on the identifying property, then SET the rest.

SET changes what exists: SET p.age = 30 writes a property, SET p:Manager adds a label (p. 1367 tags actors this way), REMOVE p:Manager takes it off.

// 1 row: Emil, now :Person:Fisherman with klout 100
MATCH (ee:Person {name: "Emil"})
SET ee:Fisherman, ee.klout = 100
RETURN labels(ee), ee.klout

Deleting starts from a MATCH: you delete what a variable points to. Page 1342 deletes Ben's KNOWS relationships, leaving the far end anonymous, (); page 1337 deletes Ben.

// deletes Ben's outgoing KNOWS relationships, not the nodes
MATCH (n {name: 'Ben'})-[r:KNOWS]->() DELETE r
// deletes the node Ben, but only if no relationship touches him
MATCH (n:Person {name: 'Ben'}) DELETE n

If Ben still knows someone, the second statement fails and nothing changes: "Cannot delete node<…>, because it still has relationships." A graph never keeps an arrow with a missing end. DETACH "removes all the relationships before removing the nodes" (p. 1343):

// deletes every node with all its relationships: an empty graph
MATCH (n) DETACH DELETE n
Ben Zoe KNOWS refused: Ben still has relationships

Before: DELETE n

Ben Zoe arrow first, then Ben

After: DETACH DELETE n

Fig. 3. MATCH (n:Person {name: 'Ben'}) followed by DELETE n or by DETACH DELETE n. Plain DELETE refuses to leave a relationship without its end; DETACH removes the relationships first (p. 1343).

Reading the graph: the movie database

Reading clauses come in a fixed order (p. 1338):

// 1 row: the node Emil
MATCH (ee:Person) WHERE ee.name = "Emil" RETURN ee;

The slide says "the MATCH clause returns all the nodes that match the conditions". Sharpen it: MATCH finds every way the pattern fits and makes one row per way, WHERE keeps the rows whose condition is true, RETURN picks the columns. "One row per way" is the key to the hook.

The exercises then switch to Neo4j's movie graph (p. 1353), the one :play movies loads into the Neo4j Browser. The slide lists "Actors", "Directors" and "Films – title, release, tagline"; in the data every person is a :Person and every film a :Movie, acting and directing are relationships, not labels, and the year is released.

The movie graph as Neo4j's script builds it: 171 nodes and 253 relationships, counted on a loaded copy. The slide lists the first four relationship types.
ElementConnectsPropertiesHow many
:Personnode labelname, born133
:Movienode labeltitle, released, tagline38
ACTED_INPerson → Movieroles (a list)172
DIRECTEDPerson → Movie—44
PRODUCEDPerson → Movie—15
WROTEPerson → Movie—10
REVIEWEDPerson → Moviesummary, rating9
FOLLOWSPerson → Person—3

The node queries of pp. 1354–1355 show two ways of filtering. A property map is an equality test, a label narrows the search, LIMIT caps the rows:

// 1 row: Tom Hanks (no label: every node is checked)
MATCH (tom {name: "Tom Hanks"}) RETURN tom
// 1 row: the film Cloud Atlas
MATCH (cloudAtlas {title: "Cloud Atlas"}) RETURN cloudAtlas
// 10 rows: the names of 10 people
MATCH (people:Person) RETURN people.name LIMIT 10

Anything other than equality needs WHERE:

// 20 rows: the films released from 1990 to 1999
MATCH (nineties:Movie)
WHERE nineties.released >= 1990 AND nineties.released < 2000
RETURN nineties.title
// 1 row: 106
MATCH (person:Person)
WHERE person.born < 1970
RETURN COUNT(person.name)

"So 133 − 106 = 27 people were born in 1970 or later." No, 22: five people have no born, so person.born < 1970 is null for them, and WHERE keeps only true rows.

Pattern queries put relationships to work (pp. 1356–1359). Read each aloud: "Tom Hanks, acted in, some movie".

// 12 rows: Tom Hanks and one of his films per row
MATCH (tom:Person {name: "Tom Hanks"})-[:ACTED_IN]->(tomHanksMovies)
RETURN tom,tomHanksMovies

The slide's picture also shows a DIRECTED arrow to That Thing You Do: the Browser draws every relationship between the nodes on screen (Tom Hanks also directed it), but the twelve rows hold only ACTED_IN matches.

// 3 rows: Tom Tykwer, Lilly Wachowski, Lana Wachowski
MATCH (cloudAtlas {title: "Cloud Atlas"})<-[:DIRECTED]-(directors)
RETURN directors.name

The "Inverse Relationship" of p. 1357 is the same arrow read from the film: (directors)-[:DIRECTED]->(cloudAtlas …) gives the same names. Two arrows meeting in one node find co-actors (p. 1358):

// 39 rows but 34 different people: Meg Ryan comes back 3 times
MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)
      <-[:ACTED_IN]-(coActors)
RETURN coActors.name

The shared film (m) does the work of a join. Tom Hanks is never his own co-actor: inside one pattern each relationship is used at most once, and his ACTED_IN to m is taken by the left arm. Meg Ryan returns three times: three shared films, three rows. Last, a relationship with a variable and no type (p. 1359):

// 10 rows: 4 ACTED_IN, 3 DIRECTED, WROTE, PRODUCED, REVIEWED
MATCH (people:Person)-[relatedTo]-(:Movie {title: "Cloud Atlas"})
RETURN people.name, Type(relatedTo), relatedTo

type() returns the type's name (function names ignore case, so Type works); the film has "No variable name" because nothing is returned from it. Try every mark of these four drawings:

The pattern is the drawing

Nine nodes of the real movie graph and all ten relationships among them. Every arrow from the top row is ACTED_IN. Pick one of the four slide queries, then change one mark of its drawing at a time; every row the pattern finds is drawn in orange and listed under the picture.

  1. Take p. 1356. Before touching anything: how many rows if every arrow is reversed? And with no arrow heads at all?
    What you should see

    Reversed: 0 rows — no ACTED_IN goes from a film to Tom Hanks. No heads: 3 rows, the same as the slide's arrow, because all of his ACTED_IN relationships already point away from him. A head is a filter only when the data has arrows both ways.

  2. Take p. 1357 and set the relationship type to "none". Predict who joins Tom Tykwer. Then set the type back to "as on the slide" and untick the property map: the variable is still called cloudAtlas — which nodes can it be now?
    What you should see

    Without a type, 4 rows: Tom Tykwer (DIRECTED), David Mitchell (WROTE), Tom Hanks and Halle Berry (ACTED_IN) — the "any relationship" of p. 1359. With the type back and the map gone, 3 rows: Nora Ephron twice and Tom Tykwer, through all three films. A variable's name filters nothing; cloudAtlas binds to any film.

  3. Take p. 1358. Predict the rows and the different co-actors on this slice. Then set the type to "none".
    What you should see

    3 rows, 2 people: Meg Ryan twice (two shared films) and Halle Berry; Tom Hanks never appears. With any type, 7 rows and 5 people: Nora Ephron (twice), Tom Tykwer and David Mitchell join, because the arms now accept DIRECTED and WROTE. Every mark you leave out lets more of the graph fit.

Rows are paths: DISTINCT, COUNT and the plan

Back to the hook. The first dataset of the session is the graph of Neo4j's getting-started guide (the slides show only the queries): Emil, Johan, Ian, Rik and Allison, joined by seven KNOWS relationships. "Let's find all of Emil's friends" (p. 1345):

// 2 rows: Emil with Johan, Emil with Ian
MATCH (ee:Person)-[:KNOWS]-(friends)
WHERE ee.name = "Emil" RETURN ee, friends

Then "a new friend that can teach surf to Johan" (p. 1346): a friend of a friend, with an anonymous middle node () because nobody needs its name.

// 1 row: Allison
MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)
WHERE js.name = "Johan" AND surfer.hobby = "surfing"
RETURN DISTINCT surfer

"What would happen if we remove the DISTINCT clause from the query?" (p. 1347). With RETURN COUNT(surfer) the answer is 3, with RETURN COUNT(DISTINCT surfer) it is 1. Three different paths of two KNOWS relationships lead from Johan to Allison: through Ian over the arrow Johan→Ian, through Ian over the arrow Ian→Johan (a second relationship, accepted by the head-less pattern), and through Rik. MATCH makes one row per path; COUNT(x) counts the rows in which x is not null, COUNT(DISTINCT x) the different values. Two numbers, two questions: how many ways, how many people.

A row is one way the drawing fits: COUNT(x) counts paths, COUNT(DISTINCT x) counts things.

One rule keeps the counting finite: within one pattern each relationship is used at most once per row, while nodes may repeat — relationship uniqueness. Johan can be his own friend-of-a-friend, out to Ian over one arrow and back over the other, but never over the same arrow twice.

Paths or people?

The getting-started graph; all seven arrows are KNOWS. The query is the one of p. 1346 with the RETURN you pick. "Next path" shows the paths one at a time, the way MATCH produces rows; the small number beside a node counts the rows that end there.

  1. With RETURN COUNT(surfer), predict the counter after the last path. Then step through and watch which arrow between Johan and Ian each path uses.
    What you should see

    3. The first and third paths both go through Ian to Allison, one over Johan→Ian, the other over Ian→Johan; the second goes through Rik. Three rows, all ending at Allison.

  2. Switch to RETURN DISTINCT surfer, then to COUNT(DISTINCT surfer). How many rows survive?
    What you should see

    One row, Allison, and the count 1. The paths are different; their last node is the same, and DISTINCT keeps each value once.

  3. Untick the hobby condition. Predict: is Johan his own friend-of-a-friend, and how many paths are there now?
    What you should see

    8 paths and 4 people: Allison 3 times, Emil and Johan twice, Ian once. Johan comes back over the other arrow to Ian, never over the one he left by: in the stage counts, 12 two-step walks drop to 8 because 4 of them reuse their first relationship.

At the exam, "the number of different X" means count(DISTINCT x): two waiters named Federico at the same table give two paths to every dish ordered there.

How does Neo4j turn the pattern into rows? PROFILE "executes the query and provides a detailed execution plan" (p. 1348). Fig. 4 redraws the plan of p. 1349: each box is an operator, and the numbers between the boxes are the rows flowing from one to the next.

NodeByLabelScan Filter Expand(All) Expand(All) Filter Distinct ProduceResults Result 5 rows 1 row 4 rows 12 rows 3 rows 1 row 1 row js:Person est. 10 · 6 db hits js.name = $autostring_0 est. 1 · 5 db hits (js)-[anon_0:KNOWS]-(anon_1) est. 1 · 5 db hits (anon_1)-[anon_2:KNOWS]-(surfer) est. 1 · 16 db hits surfer.hobby = $autostring_1 AND not anon_0 = anon_2 est. 0 · 12 db hits 3 paths in, 1 surfer out
Fig. 4. The plan of p. 1349, read from the top: scan the 5 Person nodes, keep Johan, expand to his 4 KNOWS relationships, expand again to 12 two-step walks, keep the 3 that end at a surfer without reusing a relationship, and Distinct leaves 1. Operators, details and numbers as printed on the slide; the slide lays the boxes out in three columns.

The filter not anon_0 = anon_2 is relationship uniqueness written out, and the literal "Johan" has become the parameter $autostring_0 ("Use parameters instead of literals", p. 282). "Estimated rows" are a guess from statistics; "rows" and "db hits" are measured. EXPLAIN shows the same tree without running the query, estimates only: "Use PROFILE / EXPLAIN to analyze the performance of your queries" (p. 282).

Variable-length paths and shortest paths

A star inside the brackets repeats the relationship. "Movies and actors up to 4 "hops" away from "Kevin Bacon"" (p. 1360):

// 135 rows: the nodes 1 to 4 relationships away
// (297 paths before DISTINCT)
MATCH (bacon:Person {name:"Kevin Bacon"})-[*1..4]-(hollywood)
RETURN DISTINCT hollywood

The callouts give the rules: "Distance computed w.r.t. relationships", "Any node", and "Distinct clause, as many paths may lead to the same node". "Movies and actors" undersells it: (hollywood) has no label and the brackets no type, so directors, writers, producers and reviewers come too — and Kevin Bacon himself, around a cycle of four different relationships through Apollo 13, Ron Howard and Frost/Nixon.

Variable-length patterns as p. 1374 prints them, checked in Neo4j 5.26. The last three rows are repeated on p. 1375.
On the slideThe slide's readingIn Neo4j
()-[*]->()"Directed path of any length (1 or more relationships)"correct
()-[*0..]->()"… including zero relationships"correct: the zero-length path is the start node itself
()-[*1..3]->()"Directed path of length between 1 and 3"correct
()-[*]-(), ()-[*1..]-()"Undirected path of any length" / "of length 1 or more"correct
()-[*0..2]-()"Undirected path of length between 0 and 2"correct
()-[]+->()"One or more relationships (same as [*1..])"syntax error: write ()-[]->+()
()-[]*->()"Zero or more relationships (same as [*0..])"syntax error: write ()-[]->*()
()-[]?-()"Optional relationship (zero or one)"syntax error: write ()-[*0..1]-()
MATCH p = (a)-[:FRIEND*1..3]->(b)"p is the variable holding the path from a to b through 1 to 3 FRIEND relationships"correct

The defaults matter: [*] means one or more relationships, never zero; [*0..] also matches the zero-length path, whose ends are one node; the type comes before the star, [:FRIEND*1..3]. And the rule to keep: "Always set an upper limit for your variable length patterns. It's easy to have a query touch all nodes in a graph by mistake" (p. 282).

The Browser underlines risky queries in yellow: "WARNINGS may arise … some operators may be deprecated or the query is performing a lot of operations e.g., cartesian products among the nodes or unbound number of relationships" (p. 1390). A warning is not an error; the query runs. MATCH (a:Person), (b:Person) of p. 1339 is two patterns with no relationship between them: its rows are all pairs of persons, a cartesian product, and only the WHERE narrows them to Mike and Mark (Neo4j filters each side first, so there the product is 1 × 1 and harmless). An unbounded [*] may walk the whole graph.

The shortest path between two people is a built-in function (p. 1361):

// 1 row: one path of length 4
// (six paths tie; Neo4j returns one of them)
MATCH p = shortestPath(
(bacon:Person {name:"Kevin Bacon"})-[*]-
(meg:Person {name:"Meg Ryan"})
)
RETURN p

p = … binds the whole path to a variable; RETURN p gives the "start node, end node and path". shortestPath returns one shortest path, and when several tie you cannot choose which; allShortestPaths returns all of them, and lecture 06 writes both with a bound, [*..6] (p. 280):

// 6 rows: every shortest path, all of length 4
MATCH p = allShortestPaths(
(bacon:Person {name:"Kevin Bacon"})-[*..6]-
(meg:Person {name:"Meg Ryan"})
)
RETURN p

Page 1362 binds both people first and allows one relationship type:

// as printed on p. 1362: no rows on the movie graph
MATCH (KevinB:Person {name: 'Kevin Bacon'} ),
      (Al:Person {name: 'Al Pacino'}),
      p = shortestPath((KevinB)-[:ACTED_IN*]-(Al))
WHERE all(r IN relationships(p) WHERE r.role IS NOT NULL)
RETURN p

Page 1363 takes the WHERE apart: relationships(p) "returns a list containing all the relationships in a path", IN walks r through it, and all() "returns TRUE if the predicate holds for all elements in the given list". Relatives: nodes(p), length(p) (the number of relationships), any(), none().

How Neo4j plans a shortest path

Pages 1368–1373 ask how the shortest path is searched, following Neo4j's manual: "Depending on the predicates to be evaluated, Neo4j plans the shortest path in different ways."

Why two algorithms? Breadth-first search grows one ring of neighbours at a time — here from both ends, until the two searches meet — so the first path it completes is a shortest one. A condition on every node or relationship is checked the moment an element is reached: a Movie node fails "all nodes must have the Person label", and everything behind it is "Excluded a priori" (p. 1369). A condition on at least one node cannot be: a branch through a Movie may still reach Kevin Bacon, so it "Cannot be excluded a priori" (p. 1370). Only whole paths can be judged, enumerated depth-first, one branch to its end before the next.

Pages 1371–1373 compare three queries (the tabs below). WHERE length(p) > 1 needs the whole path, so the plan includes the exhaustive fallback: if the path the fast search finds fails the predicate, the exhaustive search returns the shortest one that passes — "Predicates used in the WHERE clause that apply to the shortest path pattern are evaluated before choosing the shortest matching path" (p. 1371). Put WITH p in between and the predicate leaves the pattern: "the query plan will not include the fallback to the Slower Exhaustive Search Algorithm" (p. 1372). The fast search returns its one path; if that path is a direct link of length 1, WHERE throws it away and nothing is left.

A predicate inside the shortest-path MATCH steers the search; after WITH p it only filters the one path already found.

// fast search; "Always returns a result, if exists"
MATCH (KevinB:Person {name: 'Kevin Bacon'} ),
      (Al:Person {name: 'Al Pacino'}),
      p = shortestPath((KevinB)-[:ACTED_IN*]-(Al))
WHERE all(r IN relationships(p) WHERE r.role IS NOT NULL)
RETURN p

A condition on every relationship: checked while searching, no fallback needed (p. 1373; on the movie graph write roles).

// exhaustive fallback planned; "Always returns a result, if exists"
MATCH (KevinB:Person {name: 'Kevin Bacon'}),
      (Al:Person {name: 'Al Pacino'}),
      p = shortestPath((KevinB)-[*]-(Al))
WHERE length(p) > 1
RETURN p

"Must check that the whole path follows the predicate before validity is evaluated" (p. 1372). Neo4j announces it: "Exhaustive shortest path has been planned for your query".

// fast search only;
// "Not guaranteed to return a result, even if it exists"
MATCH (KevinB:Person {name: 'Kevin Bacon'}),
      (Al:Person {name: 'Al Pacino'}),
      p = shortestPath((KevinB)-[*]-(Al))
WITH p
WHERE length(p) > 1
RETURN p

The shortest path is chosen first, with no condition; WHERE filters that one path afterwards.

On the movie graph the three cannot be told apart (the first once role is read as roles): Kevin Bacon and Al Pacino are not linked directly, so every path between them is longer than 1, and each query returns a path of length 6. The point is the guarantee, and a toy graph with a direct link shows it. The second puzzle is solved: two words moved the predicate out of the pattern.

Fast, exhaustive, or nothing

A toy graph, not the movie data: Kevin follows Al directly, both acted in Movie A, and a longer chain runs through Ann and Bob. Only the ACTED_IN relationships carry roles. The query is MATCH (k:Person {name: 'Kevin'}), (al:Person {name: 'Al'}), p = shortestPath((k)-[*]-(al)) followed by the WHERE you pick. Step through the search; the numbers give the order in which nodes are first reached.

  1. Pick "no WHERE", then WHERE length(p) > 1. Predict the path each returns, then step through both.
    What you should see

    No WHERE: Kevin–Al, length 1, found in the first step. With length(p) > 1 the fast search finds the same Kevin–Al, the predicate rejects it, and the planned exhaustive search runs depth-first: Kevin–Al fails, Kevin–Ann–Bob–Al (length 3) passes, Kevin–Movie A–Al (length 2) passes and is shorter. Returned: length 2.

  2. Now pick WITH p WHERE length(p) > 1. Predict the result before stepping.
    What you should see

    No rows. Nothing in the pattern needed the whole path, so no fallback was planned; the fast search hands its one path, Kevin–Al, to WITH, and WHERE throws it away. The last readout says a valid path of length 2 exists all the same: the WITH moved the condition out of the search.

  3. Compare r.roles IS NOT NULL with the slide's r.role IS NOT NULL. Which relationships turn red, and when?
    What you should see

    With roles each FOLLOWS relationship is cut off the moment the search reaches it (three of the four; Ann–Bob is never reached), and the fast search alone returns Kevin–Movie A–Al. With role every relationship the search reaches is cut off, both searches get stuck, and the query returns nothing — exactly what p. 1362 does on the movie graph.

  4. Last pair: "all nodes are Person" against "at least one node is a Movie". Which one plans the fallback?
    What you should see

    "All nodes are Person" cuts off Movie A in the first step and returns Kevin–Al with the fast search alone: a universal condition. "At least one node is a Movie" cannot judge Kevin–Al until the path is complete; it fails, and the exhaustive search returns Kevin–Movie A–Al: an existential condition, like the slide's "at least one node … Kevin Bacon".

Exam-style questions

Four exercises in the wording of the past papers, each on the ER scenario of a real paper with the graph model given (designing the model is lesson 12), and two theory questions. No solutions of the papers are published: these model answers are ours, and each query was run on a small test graph in Neo4j 5.26.

Scenario (the restaurant ER of the exam of 6 Sep 2024): Person (Personal_ID, Name, Surname, …) with the child entity Waiter (Hiring Date); Table (Table_ID, Table Number, Max Seats); Dish (Dish_ID, Name, Price, Recipe, Description). A waiter serves tables (Serve); dishes are ordered at tables (Order). Assume this graph model:

(:Person:Waiter {personal_id: String, name: String, …})
  -[:SERVE]->(:Table {table_id: String, max_seats: Integer, …})
(:Table)-[:ORDER]->(:Dish {dish_id: String, name: String, …})

Write a Cypher query to count the total number of different dishes ordered at tables whose max seats is greater than 10, considering only tables served by at least one waiter whose name is "Federico".

Show model answer
MATCH (w:Waiter {name: "Federico"})-[:SERVE]->(t:Table)
      -[:ORDER]->(d:Dish)
WHERE t.max_seats > 10
RETURN count(DISTINCT d) AS differentDishes

Adapted from exercise 2 of that paper, which asks "for each table" and so needs grouping (lesson 12). What the answer must get right: one pattern for the chain waiter → table → dish, the equality inside the node, the range in WHERE, and DISTINCT. Test graph: two Federicos serve table 1 (12 seats; pasta and fish), one of them also table 2 (16 seats; fish and salad). The pattern gives 6 rows for 3 different dishes; count(d) would answer 6.

Scenario (the natural-disasters ER of the exam of 6 Jun 2024): Natural Disaster (ID, Start Time, Start Date, End Time, End Date, Disaster Description) with the total, exclusive children Earthquake (Magnitude, Mercalli Scale), Flooding (Water Height), Landslide (Landslide Class) and Tornado (EF Scale); the recursive relationship Cause ("A Natural Disaster may cause another Natural Disaster") with the attribute Causal Description. Assume each disaster is one node carrying both labels, and Cause is a relationship from the causing disaster to the caused one:

(:NaturalDisaster:Earthquake {id: String, magnitude: Float, …})
(:NaturalDisaster)-[:CAUSE {causal_description: String}]
  ->(:NaturalDisaster)

Write a Cypher query to return the earthquakes with a magnitude greater than 8 that caused at least one flooding with the causal description equal to "very strong earthquake", which in turn caused at least one landslide.

Show model answer
MATCH (e:Earthquake)-[c:CAUSE]->(f:Flooding)
      -[:CAUSE]->(:Landslide)
WHERE e.magnitude > 8
  AND c.causal_description = "very strong earthquake"
RETURN DISTINCT e

Adapted from exercise 3 of that paper, a MongoDB query there ("which in turn caused two landslides" needs a count: lesson 12). The first relationship needs a variable because the condition is on its property; the landslide stays anonymous because nothing is returned from it. DISTINCT, because a flooding that caused two landslides returns its earthquake twice — exactly what the test graph did without it.

Scenario: the same natural-disasters ER and graph model as the previous question.

Write a Cypher query to return the ID and the description of every natural disaster caused, directly or through a chain of at most three Cause relationships, by the natural disaster with ID "ND-01".

Show model answer
MATCH (d:NaturalDisaster {id: "ND-01"})
      -[:CAUSE*1..3]->(x:NaturalDisaster)
RETURN DISTINCT x.id, x.disaster_description

Three things matter here: a bounded variable-length pattern with the type before the star, the arrow, and DISTINCT. On the test chain ND-01 → ND-02 → ND-03 → ND-04 → ND-05, plus ND-01 → ND-06 and ND-07 → ND-01, the answer is ND-02, ND-03, ND-04 and ND-06. Drop the arrow head and ND-07, the disaster that caused ND-01, slips in; drop the bound and ND-05, four steps away, does too. DISTINCT costs nothing and protects you as soon as two chains can reach the same disaster.

Scenario (the hospital-heating ER of the exam of 30 Jan 2026): Building (Building_ID, Name, Year), Floor (Floor_ID, Number, Has_Elevator), Heating System (Heater_Sys_ID, Heating_Power) with the (T, E) children HVAC System and Gas Heating System; Has Floor (Building – Floor) and Heats (Heating System – Floor, with Start_Time and End_Time). Assume this graph model:

(:Building {building_id: String, name: String, year: Integer})
  -[:HAS_FLOOR]->(:Floor {floor_id: String, number: Integer,
                          has_elevator: Boolean})
(:HeatingSystem:GasHeatingSystem {heater_sys_id: String, …})
  -[:HEATS {start_time: DateTime, end_time: DateTime}]->(:Floor)

Write a Cypher query that returns the buildings built after 1967 with at least one floor with an elevator that is heated by a Gas Heating System. Return the ID and the name of each building once.

Show model answer
MATCH (b:Building)-[:HAS_FLOOR]->(f:Floor)
      <-[:HEATS]-(:GasHeatingSystem)
WHERE b.year > 1967 AND f.has_elevator = true
RETURN DISTINCT b.building_id, b.name

Adapted from exercise 1 of that paper (its query adds up beds, which needs grouping). The pattern is the co-actor V of p. 1358 with the floor in the middle, and the child label :GasHeatingSystem alone selects the gas systems. Without DISTINCT the test building came back three times: two gas systems heat one of its floors and one heats another.

On the dataset of the exercise session, this query returns 3:

MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)
WHERE js.name = "Johan" AND surfer.hobby = "surfing"
RETURN COUNT(surfer)

The same query with RETURN COUNT(DISTINCT surfer) returns 1. Explain why, and say which number answers "how many people can teach Johan to surf".

Show model answer

MATCH produces one row for every way the pattern fits the graph, that is one row per path. The only surfer, Allison, is reached from Johan by three different paths of two KNOWS relationships: through Ian over the relationship Johan→Ian, through Ian over the relationship Ian→Johan (the pattern has no direction, and these are two different relationships), and through Rik. Within one path a relationship is never used twice, but different paths may end at the same node. COUNT(surfer) counts the rows, so 3; COUNT(DISTINCT surfer) counts the different nodes, so 1. The number of people is 1. The PROFILE plan shows it: 3 rows enter the Distinct operator and 1 leaves it.

A complete answer rests on two ideas: rows are paths, not nodes, and the two KNOWS relationships between Johan and Ian count as different paths. Naming the three paths turns a vague answer into a precise one; the PROFILE numbers back it up.

True or false: in Neo4j, inserting WITH p between MATCH …, p = shortestPath((a)-[*]-(b)) and WHERE length(p) > 1 never changes the result of the query.

  • True Without WITH the predicate belongs to the shortest-path pattern and the exhaustive fallback is planned; with WITH it is a plain filter applied to the one path already chosen.
  • False If the shortest path has length 1 and a longer one exists, the query without WITH returns the longer path, the query with WITH returns nothing.
Show model answer

False. Without WITH, the predicate is evaluated as part of the shortest-path pattern; because it needs the whole path, Neo4j plans the slower exhaustive depth-first search as a fallback, so the query returns the shortest path of length greater than 1 whenever one exists. With WITH p, the fast bidirectional breadth-first search returns one shortest path with no predicate; if its length is 1, the WHERE removes it and the query returns no rows, even though a valid path exists.

A wrong answer may cost points, so tick only what you can justify. The words to remember are the slide's: "Always returns a result, if exists" against "Not guaranteed to return a result, even if it exists" (p. 1373).

Check yourself

Ann knows Bob and Bob knows Ann: two KNOWS relationships. How many rows does MATCH (x:Person)-[:KNOWS]-(y:Person) RETURN x.name, y.name return?

  • 1 There is no "pair" object: rows are ways the pattern fits, and both relationships fit in both directions.
  • 2 That is the count with an arrow head, -[:KNOWS]->: each relationship read in its own direction.
  • 4 Without a head each relationship matches twice, once with Ann as x and once with Bob as x: 2 relationships × 2 directions.
  • 8 A one-relationship pattern cannot combine two relationships in one row; 4 is the maximum.

On the movie graph, what does MATCH (p:Person)-[ACTED_IN]->(m:Movie) RETURN p.name match?

  • Only ACTED_IN relationships That needs the colon, [:ACTED_IN].
  • Every relationship from a person to a film: ACTED_IN, DIRECTED, PRODUCED, WROTE, REVIEWED Without a colon, ACTED_IN is just a variable name: "Only variable name (any relationship)", p. 1359.
  • Nothing: the query is a syntax error It is valid Cypher; a variable may be called ACTED_IN.
  • Nothing: ACTED_IN is not defined A variable needs no definition; the pattern introduces it.

Ben knows Zoe. You run MATCH (n:Person {name: 'Ben'}) DELETE n. What happens?

  • Ben and his KNOWS relationship are deleted That is DETACH DELETE.
  • Ben is deleted; the KNOWS stays with one end missing A graph never keeps a relationship without its end node.
  • The statement fails and nothing changes "Cannot delete node…, because it still has relationships": delete the relationships first, or use DETACH.
  • Ben and Zoe are both deleted Only the matched variable is deleted, and only once it has no relationships.

About MATCH (bacon:Person {name:"Kevin Bacon"})-[*1..4]-(hollywood) RETURN DISTINCT hollywood (p. 1360). Select all that are true.

  • It returns directors, writers and reviewers too, not only movies and actors (hollywood) has no label and the brackets have no type.
  • It can return Kevin Bacon himself A cycle of four different relationships leads back to him; relationship uniqueness forbids reusing a relationship, not revisiting a node.
  • Without DISTINCT it returns more rows 297 paths against 135 different nodes: many paths reach the same node.
  • [*1..4] counts nodes, not relationships "Distance computed w.r.t. relationships": 1 to 4 relationships.
  • Replacing [*1..4] with [*] would make it safer The opposite: an unbounded pattern may touch the whole graph, and the Browser warns about it.

You run the query of p. 1339 (MATCH Mike and Mark, CREATE (a)-[r:KNOWS]->(b)) twice, then the same MATCH with MERGE (a)-[:KNOWS]->(b) once. How many KNOWS relationships go from Mike to Mark?

  • 1 MERGE does not remove duplicates; it only avoids creating new ones.
  • 2 Each CREATE wrote one; MERGE found the pattern already there and wrote nothing.
  • 3 MERGE creates only when the whole pattern is missing, and here it exists.

Which WHERE makes Neo4j plan the exhaustive fallback for p = shortestPath((a)-[*]-(b))?

  • all(n IN nodes(p) WHERE n:Person) A condition on every node: checked as each node is reached, so the fast search suffices.
  • none(n IN nodes(p) WHERE n.name = "Ann") Also universal: every node must fail the test, which can be checked node by node.
  • any(n IN nodes(p) WHERE n.name = "Kevin Bacon") "At least one node" can be decided only on the whole path: the slide's own example of the exhaustive case.
  • No WHERE at all Then there is nothing to check: the fast search returns the shortest path.

You want to see how Neo4j would execute a DETACH DELETE statement without deleting anything. Which keyword do you put in front?

  • EXPLAIN It shows the plan with estimated rows and does not run the statement.
  • PROFILE PROFILE "executes the query" (p. 1348): the nodes would be gone.
  • WITH WITH joins query parts; it does not stop execution.

Key take-aways

What to watch and read