A property graph needs a query language. In Neo4j that language is Cypher: where SQL describes tables and JOINs, Cypher describes a pattern — which nodes are connected by which relationships. A query looks like a drawing of a connection written in text.
The label index is used once — it finds the start node. After that Cypher walks relationships from node to node, and the asterisk *1..2 is exactly two such hops. That is why the cost of the query depends on the number of relationships traversed, not on the size of the database.
The relationship pattern: a drawing in brackets
A node goes in round brackets, a relationship in square ones, the direction is an arrow:
(p:Person) -[:КУПИЛ]-> (t:Product)
It reads literally: "node p with the label :Person bought node t with the label :Product". The letters p and t are variables, so you can refer to them later.
MATCH and RETURN: find and return
A read starts with MATCH (find the pattern) and ends with RETURN (what to hand back). "Which products did Anna buy" — run it and change the name:
live example
MATCH (p:Person {firstName: 'Anna'}) -[:КУПИЛ]-> (t:Product)
RETURN t.title
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
A filter goes either inside the node itself ({firstName: 'Anna'}) or as a separate WHERE condition — same as in SQL; below are both at once. "Products bought together with this one" is a two-hop traversal: from the product to its buyers, from them to their other purchases:
live example
MATCH (t:Product {title: 'Механическая клавиатура'}) <-[:КУПИЛ]- (:Person) -[:КУПИЛ]-> (other:Product)
WHERE other.title <> 'Механическая клавиатура'
RETURN other.title, count(*) AS together
ORDER BY together DESC LIMIT 5
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
This is the "bought together with" block of a shop page — two relationships and one grouping, with no link table at all.
The arrow in <-[:КУПИЛ]- is reversed: from the product back to the buyer the relationship runs the other way. The direction can be dropped altogether (-[:КУПИЛ]-) — then the traversal goes both ways.
Variable-length paths: traversing to an unknown depth
The real strength of Cypher is traversal where the number of hops is not known in advance. It is written with an asterisk, as in regular expressions. "Anna's acquaintances and the acquaintances of her acquaintances":
live example
MATCH (a:Person {firstName: 'Anna'}) -[:ЗНАКОМ_С*1..2]-> (кто:Person)
RETURN кто.firstName, кто.lastName
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
* means "one or more ЗНАКОМ_С relationships". The depth is bounded as *1..2 — one to two hops (as here), * — any depth, *0.. — zero or more. Change *1..2 to *1..1, run it again, and only direct acquaintances remain. The same traversal in plain Java, with no database:
live example
import java.util.*;
public class VarLengthPath {
public static void main(String[] args) {
Map<String, List<String>> edges = Map.of("Anna", List.of("Boris", "Darya"),
"Boris", List.of("Vera"), "Darya", List.of("Egor"));
List<String> layer = List.of("Anna");
for (int hop = 1; hop <= 2; hop++) {
List<String> next = new ArrayList<>();
for (String node : layer) next.addAll(edges.getOrDefault(node, List.of()));
System.out.println("hop " + hop + ": " + next);
layer = next;
}
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
In a relational database the same question turns into a bulky recursive CTE; in Cypher it is one line. There is also a built-in shortest path search, the classic "chain of handshakes":
live example
MATCH path = shortestPath(
(a:Person {firstName: 'Anna'}) -[:ЗНАКОМ_С*]- (b:Person {firstName: 'Grigory'})
)
RETURN [узел IN nodes(path) | узел.firstName] AS chain
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
nodes(path) unfolds the path into a list of nodes, and the square brackets are a list expression, like map in ordinary code: take the name out of every node. The answer reads directly: ["Anna", "Boris", "Vera", "Grigory"] — three handshakes.
CREATE and MERGE: create and update
Data is written with CREATE (create a node or a relationship) and MERGE (create if missing, otherwise take the existing one):
CREATE (p:Person {firstName: 'Maria', status: 'ACTIVE'})
MATCH (m:Person {firstName: 'Maria'}), (t:Product {title: 'Йога-коврик'})
MERGE (m) -[:КУПИЛ {date: '2026-09-17'}]-> (t)
There is no "Run" button under these two, and that is not an oversight: the learning sandbox is open for reading only. Otherwise the first CREATE would change the graph for everyone reading the article next.
MERGE will not create a duplicate if the relationship is already there — which is why it is the tool for loading data from external sources, where the same entity arrives many times over.
Where people trip
- Forgetting
MERGEand breeding duplicates. On a repeated loadCREATEmakes a second copy of the node or the relationship. For "create if not there yet" the answer is onlyMERGE, on a unique key backed by an index or a uniqueness constraint. MATCHwith no anchor scans the whole graph. With no node carrying a label and an indexed property, Neo4j starts from a full scan. Ground the query on a concrete node that the traversal will start from.- Writing
*with no upper bound. On a dense graph such a path walks half the database — bound the depth (*1..4) unless the full traversal is deliberate.
In short
- Cypher describes a pattern: a node in round brackets, a relationship in square ones, the direction an arrow.
- Reads go through
MATCH…RETURN, filters sit inside the node or inWHERE. - The arrow can be reversed (
<-[:КУПИЛ]-) or dropped — then the traversal goes both ways. - The asterisk marks a variable-length path:
*1..3is one to three hops,shortestPathfinds the shortest chain. MERGEtakes an existing element or creates it; loading withCREATEbreeds duplicates.- Ground the query on a node with a label and an index: the index is used once, after that it is relationships all the way.
What to read next
- How Neo4j Works: the Property Graph, Nodes, Relationships and Index-Free Traversal — why a hop along a relationship is cheaper than a
JOIN. - Neo4j graph modeling: indexes, supernodes and operations — which indexes
MATCHneeds and how a graph lives in production. - Graph data in plain words: recursive SQL or a graph database — where a graph beats tables.