← Back to the section

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.

MATCH (a:Person {firstName:'Anna'}) -[:ЗНАКОМ_С*1..2]-> (x) RETURN x.firstName index :Person(firstName) Anna Boris Darya … Anna Boris Darya Vera Egor Anna 1. pattern: Anna's acquaintances, one or two hops 2. the label index finds the start node — one lookup 3. hop 1: along relationships to direct acquaintances 4. hop 2: on along relationships, no index needed

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 MERGE and breeding duplicates. On a repeated load CREATE makes a second copy of the node or the relationship. For "create if not there yet" the answer is only MERGE, on a unique key backed by an index or a uniqueness constraint.
  • MATCH with 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 in WHERE.
  • The arrow can be reversed (<-[:КУПИЛ]-) or dropped — then the traversal goes both ways.
  • The asterisk marks a variable-length path: *1..3 is one to three hops, shortestPath finds the shortest chain.
  • MERGE takes an existing element or creates it; loading with CREATE breeds 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.