Skip to content

Cypher Query Language

Cypher is a declarative query language specifically designed for interacting with graph databases in the Neo4j ecosystem. With its intuitive and expressive syntax, Cypher enables users to efficiently traverse and manipulate graph structures using a combination of pattern matching and graph traversal techniques, making it a popular choice for developers and data scientists working with graph-based data models.

PuppyGraph provides support for openCypher (version 9), an open-source implementation of Cypher.

Interactive Query UI

We will use the Interactive Query UI in the short tutorial.

Example Graph

Following the Launching PuppyGraph in Docker guide, we already setup a demo modern graph data with PuppyGraph that can be used for the rest of the document as an example. You may also use your own graph data.

Hello World

Here are some basic examples of Cypher queries:

  • MATCH (v) RETURN v all nodes (vertices).
  • MATCH ()-[e]->() RETURN e all edges.
  • MATCH (v) RETURN count(v) total number of node (vertexes).
  • MATCH ()-[e]->() RETURN count(e) total number of (directed) edges.
  • MATCH ()-[e]-() RETURN count(e) total number of edges while direction is ignored. An edge (a)-[e]->(b) is counted twice as (a,b) and (b,a) .
puppy-cypher> :> MATCH (v) RETURN v
==>[v:[_type:node,name:vadas,_id:person[v2],_label:person,age:27]]
==>[v:[_type:node,name:peter,_id:person[v6],_label:person,age:35]]
==>[v:[_type:node,name:marko,_id:person[v1],_label:person,age:29]]
==>[v:[_type:node,name:josh,_id:person[v4],_label:person,age:32]]
==>[v:[_type:node,name:ripple,_id:software[v5],lang:java,_label:software]]
==>[v:[_type:node,name:lop,_id:software[v3],lang:java,_label:software]]
puppy-cypher> :> MATCH ()-[e]->() RETURN e
==>[e:[_type:relationship,_outV:person[v1],weight:0.5,_id:knows[e7],_inV:person[v2],_label:knows]]
==>[e:[_type:relationship,_outV:person[v1],weight:1.0,_id:knows[e8],_inV:person[v4],_label:knows]]
==>[e:[_type:relationship,_outV:person[v1],weight:0.4,_id:created[e9],_inV:software[v3],_label:created]]
==>[e:[_type:relationship,_outV:person[v4],weight:1.0,_id:created[e10],_inV:software[v5],_label:created]]
==>[e:[_type:relationship,_outV:person[v4],weight:0.4,_id:created[e11],_inV:software[v3],_label:created]]
==>[e:[_type:relationship,_outV:person[v6],weight:0.2,_id:created[e12],_inV:software[v3],_label:created]]
puppy-cypher> :> MATCH (v) RETURN count(v)
==>[count(v):6]
puppy-cypher> :> MATCH ()-[e]->() RETURN count(e)
==>[count(e):6]
puppy-cypher> :> MATCH ()-[e]-() RETURN count(e)
==>[count(e):12]

Basic Pattern Matching

Working with elementId

puppy-cypher> :> MATCH (v) RETURN elementId(v)
==>[elementId(v):person[v1]]
==>[elementId(v):person[v4]]
==>[elementId(v):person[v2]]
==>[elementId(v):person[v6]]
==>[elementId(v):software[v3]]
==>[elementId(v):software[v5]]

Reading node and edge IDs

A node's or edge's ID columns (its id[] in the graph schema) identify the element. They are not properties unless the schema also lists them in attribute[]. Reading an ID column as a property, such as v.id or a.account_id, returns null, with a warning that the property is not defined on the label.

Use id(v) to read the ID. Like elementId(v), it returns the label followed by the ID value in brackets, such as person[v1]. To filter by ID, compare against the same form:

MATCH (v:person) RETURN id(v), v.name
// person[v1], marko
// person[v2], vadas
// ...

MATCH (v:person)-[e:knows]->(w) WHERE id(v) = 'person[v1]' RETURN id(e), id(w)
// knows[e7], person[v2]
// knows[e8], person[v4]

To read or filter by the plain ID value as a property (v.id, or MATCH (v:person {id: 'v1'})), add the ID column to attribute[] as well and map it in mappedField[]. See Map columns to node roles.

Compare id() with the full form, label included. A bare value matches nothing. This holds for numeric IDs too: for an Account node with ID column 1201, use id(a) = 'Account[1201]', not id(a) = 1201.

MATCH (v:person) WHERE id(v) = 'v1' RETURN v.name
// no rows

MATCH (v:person) WHERE id(v) = 'person[v1]' RETURN v.name
// marko

id() returns a string, so you can compare two IDs, for example to return each pair only once:

MATCH (a:person)-[:created]->(s:software)<-[:created]-(b:person)
WHERE id(a) < id(b)
RETURN a.name, b.name, s.name
// josh, peter, lop
// marko, josh, lop
// marko, peter, lop

CALL db.propertyKeys() lists id among the property keys. Still read IDs with id(n).

Check query warnings

A query with a misspelled label, edge type or property doesn't fail. A label or edge type that doesn't exist matches nothing. A property that isn't defined on the label reads as null. In both cases PuppyGraph returns a warning with the result. Drivers report it as a notification: the Neo4j Python driver, for example, logs it as a warning. Check the warnings when a query returns no rows or null values.

MATCH (v:persons) RETURN count(v)
// 0
// warning: Query references non-existing vertex label "persons"; matching that label will return no results.

MATCH (v:person) RETURN v.nme LIMIT 1
// null
// warning: Property "nme" is not defined on vertex label "person", so it evaluates to null. Properties defined on "person": age, name.

To see the labels and edge types of the graph, run CALL db.labels() and CALL db.relationshipTypes(). See Cypher Procedures.

Not every warning means the query is wrong. Right after a schema upload, a warning can say that the execution planner ran before statistics finished collecting. The results are still correct, and the warning stops once the statistics are collected.

Patterns for labels

Show nodes with a certain label.

puppy-cypher> :> MATCH (v:person) RETURN v
==>[v:[_type:node,name:peter,_id:person[v6],_label:person,age:35]]
==>[v:[_type:node,name:josh,_id:person[v4],_label:person,age:32]]
==>[v:[_type:node,name:vadas,_id:person[v2],_label:person,age:27]]
==>[v:[_type:node,name:marko,_id:person[v1],_label:person,age:29]]

Specifying properties

Show nodes with a certain property.

puppy-cypher> :> MATCH (v {name: 'marko'}) RETURN v
==>[v:[_type:node,name:marko,_id:person[v1],_label:person,age:29]]

Use where for more flexible filters

puppy-cypher> :> MATCH (v) WHERE v.age > 30 RETURN v
==>[v:[_type:node,name:josh,_id:person[v4],_label:person,age:32]]

Output Control

Returning certain properties

puppy-cypher> :> MATCH (v) WHERE v.age > 30 RETURN v.name, v.age
==>[v.name:josh,v.age:32]
==>[v.name:peter,v.age:35]

Using lists

puppy-cypher> :> MATCH (v) WHERE v.name in ['marko', 'josh', 'peter'] AND v.age > 30 RETURN [v.name, v.age]
==>[[v.name, v.age]:[josh,32]]
==>[[v.name, v.age]:[peter,35]]

Unwinding a list

puppy-cypher> :> UNWIND ['a', 'b', 'b'] as x RETURN x
==>[x:a]
==>[x:b]
==>[x:b]

DISTINCT

puppy-cypher> :> UNWIND ['a', 'b', 'b'] as x RETURN DISTINCT x
==>[x:a]
==>[x:b]

SKIP and LIMIT

SKIP is to skip the first several results, and LIMIT is to limit the number of results.

puppy-cypher> :> MATCH (v:person) RETURN v SKIP 1 LIMIT 2
==>[v:[_type:node,name:marko,_id:person[v1],_label:person,age:29]]
==>[v:[_type:node,name:peter,_id:person[v6],_label:person,age:35]]

Showing paths

Assign the path to a variable.

puppy-cypher> :> MATCH p=(u:person)-->(v:person) RETURN p
==>[p:[_elements:[[_type:node,name:marko,_id:person[v1],_label:person,age:29],[_type:relationship,weight:0.5,_outV:person[v1],_id:knows[e7],_label:knows,_inV:person[v2]],[_type:node,name:vadas,_id:person[v2],_label:person,age:27]],_type:path]]
==>[p:[_elements:[[_type:node,name:marko,_id:person[v1],_label:person,age:29],[_type:relationship,weight:1.0,_outV:person[v1],_id:knows[e8],_label:knows,_inV:person[v4]],[_type:node,name:josh,_id:person[v4],_label:person,age:32]],_type:path]]

More complex queries

Searching for a Graph Pattern

Who are marko's co-creators?

puppy-cypher> :> MATCH (u:person {name:'marko'})-[:created]->(:software)<-[:created]-(v:person) RETURN v.name
==>[v.name:josh]
==>[v.name:peter]

Note that in a pattern, an edge is only used once. So marko is not his own co-creator. See Uniqueness section of openCypher (version 9) for more information.

Variable-length pattern matching

Here is another query to get marko's co-creators. The direction of edges is ignored.

puppy-cypher> :> MATCH (u:person {name:'marko'})-[:created*2]-(v) RETURN v.name
==>[v.name:josh]
==>[v.name:peter]

Using WITH to pipe results

puppy-cypher> :> MATCH ({name: 'marko'})-[:knows]->(v) WITH v MATCH (v)-[:created]->(x) RETURN v.name AS otherPerson, x.name AS Software
==>[otherPerson:josh,Software:ripple]
==>[otherPerson:josh,Software:lop]

We also use AS to rename the results here.

Path functions

nodes(p), relationships(p) and length(p) work on a path variable. List comprehensions work on their results.

MATCH p=(u:person {name: 'marko'})-[*1..2]->(x)
RETURN [n IN nodes(p) | n.name] AS names, [e IN relationships(p) | type(e)] AS types, length(p) AS hops
ORDER BY hops, x.name
// [marko, josh], [knows], 1
// [marko, lop], [created], 1
// [marko, vadas], [knows], 1
// [marko, josh, lop], [knows, created], 2
// [marko, josh, ripple], [knows, created], 2

shortestPath() is supported:

MATCH p=shortestPath((u:person {name: 'vadas'})-[*..4]-(x:software {name: 'ripple'}))
RETURN [n IN nodes(p) | n.name]
// [vadas, marko, josh, ripple]

reduce() is supported (PuppyGraph 1.16.0 or later, experimental). Over a list built at query time, such as relationships(p) or a collect() result, it supports adding or subtracting numbers, building a string or list with +, AND, OR, and picking the minimum or maximum with CASE. Other expressions over such a list return an error.

MATCH p=(u:person {name: 'marko'})-[*1..2]->(x)
RETURN [n IN nodes(p) | n.name] AS names, reduce(w = 0.0, e IN relationships(p) | w + e.weight) AS weight
ORDER BY weight
// [marko, lop], 0.4
// [marko, vadas], 0.5
// [marko, josh], 1.0
// [marko, josh, lop], 1.4
// [marko, josh, ripple], 2.0

Finding cycles

A fixed-length pattern that ends at its start node finds cycles. Each cycle matches once per starting node. To keep one row per cycle, require the start node to have the smallest ID. A pattern can also pass through the same node twice, so require the other nodes to differ. Parallel edges between the same nodes give repeated rows, so use RETURN DISTINCT. For example, on a graph of Account nodes and TRANSFER edges, this finds each ring of 5 different accounts once:

MATCH (a1:Account)-[:TRANSFER]->(a2:Account)-[:TRANSFER]->(a3:Account)
      -[:TRANSFER]->(a4:Account)-[:TRANSFER]->(a5:Account)-[:TRANSFER]->(a1)
WHERE id(a1) < id(a2) AND id(a1) < id(a3) AND id(a1) < id(a4) AND id(a1) < id(a5)
  AND a2 <> a3 AND a2 <> a4 AND a2 <> a5 AND a3 <> a4 AND a3 <> a5 AND a4 <> a5
RETURN DISTINCT [id(a1), id(a2), id(a3), id(a4), id(a5)] AS ring
// [Account[1201], Account[1202], Account[1203], Account[1204], Account[1205]]

Compare id() values here, not ID columns read as properties such as a1.account_id. Those read as null (see Reading node and edge IDs), so the filter drops every row.

Aggregation

collect(DISTINCT ...)

A WITH or RETURN can hold more than one collect(DISTINCT ...) (PuppyGraph 1.17.0 or later). Other aggregations, such as count(DISTINCT ...), count(), sum() and avg(), can sit next to a collect(DISTINCT ...) of values:

MATCH (p:person)-[:created]->(s:software)
RETURN s.name, collect(DISTINCT p.name) AS creators, collect(DISTINCT p.age) AS ages, count(DISTINCT p) AS n, avg(p.age) AS avg_age
// lop, [peter, josh, marko], [35, 32, 29], 3, 32.0
// ripple, [josh], [32], 1, 32.0

A collect(DISTINCT ...) of nodes, relationships or maps can't sit next to another aggregation, such as count(). The query fails with The 'collect distinct' function does not work with other aggragate functions right now. Collect the IDs instead, or collect in a separate WITH:

MATCH (a:Account)-[:TRANSFER]->(b:Account)
RETURN id(b) AS dst, collect(DISTINCT a) AS senders, count(*) AS n
// error

MATCH (a:Account)-[:TRANSFER]->(b:Account)
RETURN id(b) AS dst, collect(DISTINCT id(a)) AS senders, count(*) AS n
// Account[1202], [Account[1201], Account[1206]], 2
// ...

UNWIND after MATCH

To UNWIND a list built from matched variables, pass the variables on with WITH first. Without it, the query fails with Variable not found.

MATCH p=(u:person {name: 'marko'})-[*1..2]->(x)
WITH p
UNWIND relationships(p) AS e
RETURN sum(e.weight)
// 5.3

Sorting

ORDER BY needs a single value, such as a number or a string. Sorting by a list fails with Can not order by a pure list. Sort by a scalar column instead, as in the ORDER BY hops, x.name example above.

Edge direction

startNode() and endNode() are not supported on matched relationships. They work only on the relationships that CALL db.schema.visualization() returns (PuppyGraph 1.17.0 or later). To tell incoming edges from outgoing ones, match each direction with a directed pattern and combine the results with UNION ALL:

MATCH (v:person {name: 'josh'})-[e]->(x) RETURN 'out' AS dir, type(e) AS type, x.name AS other
UNION ALL
MATCH (v:person {name: 'josh'})<-[e]-(x) RETURN 'in' AS dir, type(e) AS type, x.name AS other
// in, knows, marko
// out, created, lop
// out, created, ripple

Decimal properties

A property read from a decimal column, such as PostgreSQL numeric(12,2), works in comparisons, ORDER BY, sum() and avg(). Adding two decimal values fails with Unsupported ExprType, and toFloat() does not accept them. Multiply each value by 1.0 first. This turns it into a floating-point number:

MATCH (a:Account)-[t1:TRANSFER]->(:Account)-[t2:TRANSFER]->(:Account)
WHERE id(a) = 'Account[1201]'
RETURN t1.amount * 1.0 + t2.amount * 1.0 AS moved
ORDER BY moved DESC
// 18900.0
// 9120.5
// 271.0

Unsupported functions

Function Error Use instead
startNode(), endNode() Unsupported Function startnode, Unsupported Function endnode Directed patterns, see Edge direction.

Right after start

Right after PuppyGraph starts, Cypher queries can fail for up to a minute with Cypher engine unavailable. Wait until GET /readyz returns 200 (PuppyGraph 1.15.0 or later), then query:

until curl -sf -o /dev/null http://localhost:8081/readyz; do sleep 2; done

Official Specification

For detailed Cypher language specification and more resources, please refer to the openCypher official website.