Skip to main content

List Functions

Lists are first-class values. Indexing is 0-based and supports negative indices counted from the end. List functions generally return null on null input.

Overview

GoalFunction / Syntax
Sizesize(list)
First, rest, lasthead, tail, last
Reversereverse(list)
Rangerange(start, end[, step])
Fold[`reduce(acc, x IN list
Index / slicelist[i], list[a..b]
Concatlist + list, list + x, x + list
Filter / map[`[x IN list WHERE …
Attach related entitiesPattern comprehension
Quantifyall, any, none, single
Collect rowscollect(expr)
Unwind rowsUNWIND list AS row

size / head / tail / last

FunctionBehaviour
size(list)Number of elements; nullnull
head(list)First element; empty list → null
tail(list)All but first; empty list → null
last(list)Last element; empty list → null
RETURN size([1, 2, 3])            -- 3
RETURN head([1, 2, 3]) -- 1
RETURN tail([1, 2, 3]) -- [2, 3]
RETURN last([1, 2, 3]) -- 3
RETURN head([]) -- null
RETURN size([]) -- 0

size also works on strings — see String Functions → size.

reverse

Works on lists and strings.

RETURN reverse([1, 2, 3])         -- [3, 2, 1]
RETURN reverse('abc') -- 'cba'

range

range(start, end[, step]) — inclusive, integers only.

RETURN range(1, 5)                -- [1, 2, 3, 4, 5]
RETURN range(0, 10, 2) -- [0, 2, 4, 6, 8, 10]
RETURN range(10, 1, -1) -- [10, 9, 8,, 1]
RETURN range(1, 5, 0) -- null (zero step)

Common uses

// Pagination helper: generate page numbers
RETURN range(1, toInteger(ceil($total / $size))) AS pages

// Simple synthetic data
UNWIND range(1, 100) AS i
CREATE (:Point {id: i, x: rand() * 100, y: rand() * 100})

reduce

Fold over a list, carrying an accumulator.

RETURN reduce(total = 0, x IN [1, 2, 3, 4] | total + x)    -- 10
RETURN reduce(pairs = [], x IN [1, 2, 3] | pairs + [x * 2]) -- [2, 4, 6]

Word frequency with a map accumulator

RETURN reduce(
acc = {},
x IN ['red', 'red', 'blue', 'green'] |
acc + {[x]: coalesce(acc[x], 0) + 1}
)
-- {red: 2, blue: 1, green: 1}

acc starts as an empty map. Each element rebuilds acc by merging in the updated count for xcoalesce(acc[x], 0) falls back to zero on the first occurrence of a key. The dynamic-key form {[x]: …} is what makes this a real histogram rather than a single x column.

Nested / layered lists

// Flatten a list-of-lists
RETURN reduce(out = [], xs IN [[1, 2], [3, 4], [5]] | out + xs)
-- [1, 2, 3, 4, 5]

reduce has no short-circuit — the whole list is visited even if the accumulator could stop early. Use any / all for short-circuit boolean questions.

Indexing and slicing

RETURN [10, 20, 30][0]       -- 10
RETURN [10, 20, 30][-1] -- 30
RETURN [10, 20, 30][5] -- null (out of range)

RETURN [1, 2, 3, 4, 5][1..3] -- [2, 3] (end-exclusive)
RETURN [1, 2, 3, 4, 5][..2] -- [1, 2]
RETURN [1, 2, 3, 4, 5][3..] -- [4, 5]
RETURN [1, 2, 3, 4, 5][-2..] -- [4, 5]

Slicing recipes

// Top 3
RETURN collect(x)[..3]

// Last 3
RETURN collect(x)[-3..]

// Second-to-fifth
RETURN collect(x)[1..5]

Concatenation

RETURN [1, 2] + [3, 4]           -- [1, 2, 3, 4]
RETURN 0 + [1, 2] -- [0, 1, 2]
RETURN [1, 2] + 3 -- [1, 2, 3]

List comprehension

-- Filter
RETURN [x IN [1, 2, 3, 4] WHERE x > 2] -- [3, 4]

-- Map
RETURN [x IN [1, 2, 3] | x * 10] -- [10, 20, 30]

-- Filter + map
RETURN [x IN [1, 2, 3, 4] WHERE x > 2 | x * 10] -- [30, 40]

On node properties

MATCH (u:User)
RETURN u.name,
[t IN u.tags WHERE size(t) > 3] AS long_tags

Nested comprehension

UNWIND [[1, 2], [3, 4], [5, 6]] AS pair
RETURN [x IN pair WHERE x % 2 = 0 | x * 10]

Pattern comprehension

Bind a pattern and collect one value per match — inline.

MATCH (p:Person)
RETURN p.name,
[(p)-[:KNOWS]->(f) | f.name] AS friends

With a filter:

MATCH (p:Person)
RETURN p.name,
[(p)-[:WROTE]->(post:Post) WHERE post.published | post.title] AS posts

Returns a list per matched p — ideal when you'd otherwise add a new MATCH stage and an aggregation just to assemble "each owner's items".

Take top-N inline

MATCH (u:User)
RETURN u.name,
[(u)-[:WROTE]->(p) | p.title][..3] AS recent_titles

Pattern comprehension vs OPTIONAL MATCH

WantUse
A list per outer row, even if emptyPattern comprehension
A row per match, with nulls for no-matchOPTIONAL MATCH
Existence only, no valuesEXISTS { … }

Predicates (in WHERE)

These are part of WHERE but operate over lists — included here as a cross-reference.

PredicateDescription
all(x IN list WHERE pred)true if pred holds for every element
any(x IN list WHERE pred)true if pred holds for at least one
none(x IN list WHERE pred)true if pred holds for none
single(x IN list WHERE pred)true if pred holds for exactly one
MATCH (n)
WHERE all(x IN n.scores WHERE x >= 0)
RETURN n

MATCH (n)
WHERE any(x IN n.tags WHERE x = 'featured')
RETURN n

MATCH (n)
WHERE single(x IN n.roles WHERE x = 'owner')
RETURN n

Predicates on paths

nodes(p) and relationships(p) are lists, so path predicates work too:

MATCH p = (a)-[:FOLLOWS*1..3]->(b)
WHERE all(r IN relationships(p) WHERE r.active)
RETURN p

See Paths.

Common patterns

Distinct values from a list

There's no built-in distinct(list). Use UNWIND + collect(DISTINCT …):

UNWIND [1, 2, 2, 3, 3, 3] AS x
RETURN collect(DISTINCT x) -- [1, 2, 3]

Sort a list

No in-place sort. Unwind, order, re-collect:

UNWIND [3, 1, 4, 1, 5, 9, 2, 6] AS x
WITH x ORDER BY x
RETURN collect(x) -- [1, 1, 2, 3, 4, 5, 6, 9]

Zip two lists (best effort)

WITH ['a', 'b', 'c'] AS keys, [1, 2, 3] AS vals
RETURN [i IN range(0, size(keys) - 1) | [keys[i], vals[i]]]
-- [['a', 1], ['b', 2], ['c', 3]]

Reduce to a single map

WITH [{k: 'a', v: 1}, {k: 'b', v: 2}] AS kvs
RETURN reduce(m = {}, kv IN kvs | m + {[kv.k]: kv.v})
-- {a: 1, b: 2}

Running totals

WITH [10, 20, 30, 40] AS xs
RETURN reduce(
acc = {total: 0, running: []},
x IN xs |
{
total: acc.total + x,
running: acc.running + [acc.total + x]
}
).running AS running_totals
-- [10, 30, 60, 100]

The accumulator is a map with two fields: total keeps the running sum between steps, running appends the new total on each step. The trailing .running picks that second field off the final map so the caller gets the sequence of totals, not the map wrapper. reduce has no short-circuit, so the full list is always visited — use it when you genuinely need every step.

Min-by on objects

min() is an aggregate — for "pick the list element with the smallest key" within a single row, use reduce:

WITH [{name: 'a', score: 9},
{name: 'b', score: 3},
{name: 'c', score: 7}] AS rows
RETURN reduce(
best = rows[0],
r IN tail(rows) |
CASE WHEN r.score < best.score THEN r ELSE best END
) AS winner
-- {name: 'b', score: 3}

Start with the first element as best, walk tail(rows), and on each step keep whichever of best vs. r has the smaller score. The CASE expression returns a whole map — the same shape as best — so the accumulator type is stable. Swap < for > to turn this into a max-by.

Bucket counts

WITH [1, 7, 12, 3, 45, 9, 22] AS xs
RETURN reduce(
buckets = {small: 0, medium: 0, large: 0},
x IN xs |
CASE
WHEN x < 10 THEN buckets + {small: buckets.small + 1}
WHEN x < 30 THEN buckets + {medium: buckets.medium + 1}
ELSE buckets + {large: buckets.large + 1}
END
) AS histogram

Slice the first N of each group

MATCH (u:User)-[:WROTE]->(p:Post)
WITH u, p ORDER BY p.published_at DESC
WITH u, collect(p)[..3] AS recent_three
RETURN u.name, [p IN recent_three | p.title]

Edge cases

Out-of-range index

RETURN [1, 2, 3][99]       -- null
RETURN [1, 2, 3][-99] -- null

Slice with reversed bounds

RETURN [1, 2, 3, 4][3..1]   -- []   (empty, not null, not an error)

Operations on null lists

Every list function returns null on a null input, including size.

RETURN size(null), head(null), tail(null)    -- null, null, null

Heterogeneous lists

Nothing enforces element-type uniformity.

RETURN [1, 'two', true, null]

Use a valueType check in all(… WHERE …) if you need a guarantee.

Limitations

  • List element types are not enforced — [1, 'two', true] is a perfectly valid list. Use all(x IN list WHERE valueType(x) = 'INTEGER') if you need homogeneity.
  • reduce has no short-circuit — the whole list is visited even if a condition could stop it early.
  • No built-in distinct(list) helper — use collect(DISTINCT x) on an unwound list.

See also