Skip to content

RETURN and Aggregation

Transform and aggregate query results.

RETURN Clause

Returning Nodes

MATCH (p:Person)
RETURN p

Returning Properties

// Single property
MATCH (p:Person)
RETURN p.name

// Multiple properties
MATCH (p:Person)
RETURN p.name, p.age, p.email

Aliases

// Using AS keyword
MATCH (p:Person)
RETURN p.name AS personName, p.age AS years

// Implicit alias (without AS - not recommended)
MATCH (p:Person)
RETURN p.name personName

Distinct Results

// Remove duplicates
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN DISTINCT friend.name

Expressions

// Computed values
MATCH (p:Person)
RETURN p.name, p.age + 1 AS nextYearAge

// String concatenation
MATCH (p:Person)
RETURN p.firstName + ' ' + p.lastName AS fullName

Arithmetic uses + - * / % ^ with standard precedence; see Numeric Functions.

Standalone RETURN

A query can consist of just a RETURN (no MATCH), which is handy for evaluating expressions:

RETURN 1 + 1 AS x            // [{'x': 2}]
RETURN toUpper('abc') AS x   // [{'x': 'ABC'}]

Aggregation Functions

COUNT

// Count all matches
MATCH (p:Person)
RETURN COUNT(p) AS personCount

// Count non-NULL values
MATCH (p:Person)
RETURN COUNT(p.email) AS withEmail

// Count all rows (including NULLs)
MATCH (p:Person)
RETURN COUNT(*) AS total

// Count distinct
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
RETURN COUNT(DISTINCT c.name) AS companyCount

SUM

// Total of all ages
MATCH (p:Person)
RETURN SUM(p.age) AS totalAge

// Total salary by department
MATCH (e:Employee)
RETURN e.department, SUM(e.salary) AS totalSalary

AVG

// Average age
MATCH (p:Person)
RETURN AVG(p.age) AS averageAge

// Average by group
MATCH (p:Person)
RETURN p.city, AVG(p.age) AS avgAge

MIN / MAX

// Minimum and maximum
MATCH (p:Person)
RETURN MIN(p.age) AS youngest, MAX(p.age) AS oldest

// By city
MATCH (p:Person)
RETURN p.city, MIN(p.age), MAX(p.age)

COLLECT

// Collect into list
MATCH (p:Person)
RETURN COLLECT(p.name) AS allNames

// Collect with filter
MATCH (p:Person)
WHERE p.city = 'NYC'
RETURN COLLECT(p.name) AS nyNames

// Nested collect
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
RETURN c.name, COLLECT(p.name) AS employees

Standard Deviation

// Sample standard deviation
MATCH (p:Person)
RETURN stdDev(p.age) AS ageStdDev

// Population standard deviation
MATCH (p:Person)
RETURN stdDevP(p.age) AS agePopulationStdDev

Percentiles

// Continuous percentile (interpolated)
MATCH (p:Person)
RETURN percentileCont(p.age, 0.5) AS medianAge

// Discrete percentile (actual value)
MATCH (p:Person)
RETURN percentileDisc(p.age, 0.9) AS p90Age

Grouping Results

Implicit Grouping

// Group by company
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
RETURN c.name, AVG(p.age) AS avgAge

// Multiple group keys
MATCH (p:Person)
RETURN p.city, p.department, COUNT(*) AS count

WITH for Pre-aggregation

// Filter after aggregation
MATCH (p:Person)
WITH p.city AS city, COUNT(*) AS cityCount
WHERE cityCount > 10
RETURN city, cityCount

WITH Scope

A WITH clause projects, then filters — so its own WHERE sees the aliases that clause defines:

MATCH (p:Person)
WITH p, p.age * 2 AS doubled
WHERE doubled > 50
RETURN p.name, doubled

WITH accepts the same expressions RETURN does: arithmetic, comparisons, function calls, patterns.

The rows the predicate sees also keep the bindings that came into the clause, so filtering on a variable the WITH did not carry forward works:

MATCH (p:Person)-[r:KNOWS]->(other)
WITH p
WHERE r.weight > 5        // `r` is not projected, but still visible
RETURN p.name

Standard Cypher drops those from scope and would reject the second query. This is deliberately more permissive.

ORDER BY, SKIP and LIMIT attach to whichever clause they follow. Written after a WITH they sort what that clause projects; written after the trailing RETURN they sort its output, and can name either its columns or the names the WITH carried:

MATCH (p:Person)
WITH p, p.age AS age
ORDER BY age DESC LIMIT 10        -- the WITH's: top ten by age
RETURN p.name AS name
ORDER BY name                      -- the RETURN's: those ten, alphabetical

Returning Everything

* stands for every variable currently bound:

MATCH (p:Person)-[r:KNOWS]->(other)
RETURN *                          -- p, r and other

MATCH (p:Person)-[r:KNOWS]->(other)
WITH *                            -- carry all three forward
WHERE r.weight > 5
RETURN p.name

It combines with named items, which is the usual way to add something without listing what you already have:

MATCH (p:Person)
WITH *, p.age * 2 AS doubled
WHERE doubled > 50
RETURN p.name, doubled

* means variables, not the columns an earlier stage produced: after WITH p, p.age AS age, a following WITH * carries p and age, and nothing named p.name.

* with an aggregate groups by everything in scope

WITH *, count(m) AS friends groups by every bound variable, m included — so it produces one row per (n, m) pair, not one row per n. When you want a count per n, name the grouping key: WITH n, count(m) AS friends.

Advanced Return Patterns

Conditional Values

// Using CASE
MATCH (p:Person)
RETURN p.name,
CASE
  WHEN p.age < 18 THEN 'minor'
  WHEN p.age < 65 THEN 'adult'
  ELSE 'senior'
END AS category

Complex Expressions

// Computed columns
MATCH (p:Person)
RETURN
  p.name,
  p.age,
  p.age * 12 AS ageInMonths,
  date().year - p.birthYear AS calculatedAge

Returning Maps

// Create map from properties
MATCH (p:Person)
RETURN {
  name: p.name,
  age: p.age,
  city: p.city
} AS personMap

Returning Paths

// Return full path
MATCH p = (a:Person)-[:KNOWS*1..3]->(b:Person)
RETURN p, length(p) AS hops

// Path nodes and relationships
MATCH p = (a:Person)-[:KNOWS]->(b:Person)
RETURN nodes(p) AS pathNodes, relationships(p) AS pathRels

Path Functions

nodes()

Returns a list of all nodes in a path.

MATCH p = (a:Person {name: 'Alice'})-[:KNOWS*1..3]->(b:Person)
RETURN nodes(p) as pathNodes
// [(:Person {name: 'Alice'}), (:Person {...}), ...]

// Count nodes in path
MATCH p = shortestPath((a)-[:KNOWS*]-(b))
RETURN size(nodes(p)) as nodeCount

relationships()

Returns a list of all relationships in a path.

MATCH p = (a:Person {name: 'Alice'})-[:KNOWS*1..3]->(b:Person)
RETURN relationships(p) as pathRels
// [(:KNOWS {...}), (:KNOWS {...}), ...]

// Get relationship types
MATCH p = (a)-[*1..3]-(b)
RETURN [r IN relationships(p) | type(r)] as relTypes

length()

Returns the number of relationships in a path. length() also accepts a string or list and returns its length.

MATCH p = (a)-[:KNOWS*]->(b)
RETURN length(p) as hops

Graph and Scalar Functions

These functions inspect nodes and relationships:

Function Returns
labels(n) List of a node's labels
type(r) A relationship's type
properties(x) Property map of a node/relationship (or the map itself)
keys(x) List of property names
id(x) Integer identity of a node/relationship
elementId(x) Identity as a string
startNode(r) / endNode(r) The source / target node of a relationship
MATCH (n:Person {name: 'Alice'})
RETURN labels(n) AS labels, properties(n) AS props, id(n) AS id

MATCH ()-[r]->()
RETURN type(r) AS rel_type, startNode(r).name AS from, endNode(r).name AS to

labels() can also be used as a filter, e.g. WHERE 'Admin' IN labels(n).

Common Use Cases

Dashboard Metrics

// User statistics
MATCH (u:User)
RETURN
  COUNT(*) AS totalUsers,
  COUNT(u.verifiedEmail) AS verifiedUsers,
  AVG(u.loginCount) AS avgLogins,
  MAX(u.lastLogin) AS mostRecentLogin

Reports

// Monthly signups
MATCH (u:User)
WITH date.truncate('month', u.createdAt) AS month, COUNT(*) AS signups
RETURN month, signups
ORDER BY month

Network Analysis

// Most connected people
MATCH (p:Person)
WITH p, COUNT { (p)-[:KNOWS]-() } AS connections
RETURN p.name, connections
ORDER BY connections DESC
LIMIT 10