Explain when aggregation collapses many matched paths into one summary row per group.
Use count, collect, sum, avg, min, and max correctly.
Predict Cypher’s implicit grouping keys from non-aggregated expressions in RETURN or WITH.
Place WITH as a grouping boundary so intermediate row multiplication does not corrupt later aggregates.
Apply DISTINCT and count(DISTINCT …) to avoid inflated path counts.
Read null-aware aggregation behavior for missing properties and optional matches.
Write multi-step analytical queries against the Movie graph that mix filtering and aggregation safely.
Core concepts
Why aggregation exists in a graph query language
Pattern matching is excellent at expanding connectivity: one actor who starred in four
movies can produce four rows; co-actor queries can explode further when two people share
several films. Business questions rarely want every intermediate path. They want
summaries—“how many movies did Tom Hanks act in?”, “what is the average review score for
The Matrix?”, “list every director with the titles they directed.” Aggregation is the
tool that turns a bag of matched paths into counts, totals, extremes, and collected lists.
In Cypher, aggregation is expression-driven rather than clause-driven like SQL’s explicit
GROUP BY. When a WITH or
RETURN contains aggregating functions, every non-aggregating
expression in that clause becomes an implicit grouping key. Rows that share the same key
values form one group; each aggregating function reduces the group to a single value.
Understanding that rule is more important than memorizing function names.
All worked examples in this course assume the classic Neo4j Movie graph: nodes labeled
Person and Movie, with
relationships such as ACTED_IN,
DIRECTED, PRODUCED,
WROTE, and REVIEWED.
Property names like name, title,
released, and rating match
the public Movie dataset conventions so you can paste queries into Neo4j Browser or Aura
after loading the sample.
Core aggregating functions
count(*) counts rows in the group. Use it when every
matched path should contribute, including rows that carry null properties.
count(expression) counts only rows where the expression
is non-null—critical after OPTIONAL MATCH or when a
property may be missing. Prefer count(DISTINCT x) when
path multiplicity would otherwise count the same entity twice.
collect(expression) builds a list of non-null values from
the group. Empty groups yield an empty list, not null. Lists are first-class values: you
can return them, pass them through WITH, later
UNWIND them (Lesson 07), or filter them with list
comprehensions (Lesson 09).
Numeric reducers—sum, avg,
min, and max—ignore null
inputs. avg over zero non-null values yields null. Mixing
types that cannot compare or sum will fail at runtime; keep review scores numeric and
release years integers when you plan to aggregate them.
CYPHER
// Implicit grouping key: person name; ORDER BY before collect for stable listsMATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WITH p, m
ORDER BY m.title
RETURN p.name AS actor,
count(m) AS movies, // rows per actorcollect(m.title) AS titles, // titles in title ordermin(m.released) AS earliest,
max(m.released) AS latest
ORDER BY movies DESC
The query above never writes GROUP BY p.name. Cypher
infers the group because p.name is not wrapped in an
aggregating function while the other returned expressions are. If you accidentally return
both p.name and m.title
without aggregating the title, you are not grouping by actor alone—you are returning one
row per actor–movie pair.
WITH as the grouping boundary
Real analytical pipelines almost never finish in a single clause. You filter people, expand
movies, count them, then filter groups with high counts, then expand again. Each time you
introduce aggregation, you must decide which bindings survive. That decision lives on
WITH.
Think of WITH as a pipeline valve: only the expressions
you project remain available downstream, and aggregation on that valve collapses rows
before the next MATCH. Placing aggregation too late lets
intermediate cartesian-style growth inflate counts. Placing it too early drops variables
you still need—unless you collect them into lists first.
Filtering after aggregation uses WHERE on a subsequent
clause (or a WITH … WHERE form). That is Cypher’s analogue
of SQL HAVING: first form groups, then keep groups that
satisfy a predicate on the aggregate.
CYPHER
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WITH p, count(m) AS movieCount
WHERE movieCount >= 3RETURN p.name AS actor, movieCount
ORDER BY movieCount DESC
DISTINCT and path multiplicity
Graphs create legitimate duplicate logical entities across different paths. If you match
(a:Person)-[:ACTED_IN]->(m)<-[:ACTED_IN]-(b:Person) to
find co-actors, Alice and Bob may share three movies and appear three times. A plain
count(b) then answers “how many co-acting path rows exist,”
not “how many unique co-actors exist.” Use count(DISTINCT b)
or project distinct pairs before counting.
RETURN DISTINCT deduplicates entire result rows after
projection. It is not a substitute for understanding multiplicity inside aggregation, but
it is useful when you only need unique projected maps or values. Prefer precise
DISTINCT inside aggregations when the goal is a correct
count rather than a cosmetic unique list.
Version note: aggregation semantics for null skipping and list collection have been stable
for years, but planner choices and available aggregating functions can grow across Neo4j
releases. Always check the aggregating functions page of the Cypher Manual for your
server major version when adopting newer helpers.
Many ACTED_IN paths expand first; aggregation collapses them into one summary row per actor group.
Worked examples
These examples use concrete Movie-graph identities. Expected tables match the classic Neo4j Movie
graph (37 movies, 133 people). If your database has been edited or uses a different seed, counts
and row order may differ.
Dataset note: Expected results use the classic Neo4j Movie graph and can differ
if the data changes.
01
Count movies per actor
Expand every ACTED_IN relationship, group by person name, and
rank actors by how many movies they appear in. This is the aggregation “hello world” for graphs.
CYPHER
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN p.name AS actor, count(m) AS movieCount
ORDER BY movieCount DESC, actor
LIMIT5
Expected result
actor
movieCount
Tom Hanks
12
Keanu Reeves
6
Hugo Weaving
5
Jack Nicholson
5
Meg Ryan
5
02
Collect cast lists and release extremes
For each movie released in or after 2000, report cast size and a collected list of actor names.
Order actors by name before collect so the list is deterministic.
This shows collect and count together.
CYPHER
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
WHERE m.released >= 2000WITH m, p
ORDER BY p.name
RETURN m.title AS title,
m.released AS released,
count(p) AS castSize,
collect(p.name) AS cast
ORDER BY released, title
LIMIT3
Reviewers leave REVIEWED relationships with a
rating property. Compute average, count, min, and max in the
same WITH grouping boundary—before rows collapse—then keep
movies with at least two reviews. Do not call min(avgRating)
after aggregation; low and high must come from r.rating in
the same aggregating clause as avg.
CYPHER
MATCH (m:Movie)<-[r:REVIEWED]-(:Person)
WITH m,
avg(r.rating) AS avgRating,
count(r) AS reviews,
min(r.rating) AS low,
max(r.rating) AS high
WHERE reviews >= 2RETURN m.title AS title,
round(avgRating, 1) AS avgRating,
reviews, low, high
ORDER BY avgRating DESC
Expected result
Only movies with at least two REVIEWED edges appear.
One-review titles (for example Cloud Atlas, Jerry Maguire, The Birdcage, Unforgiven) are filtered out.
title
avgRating
reviews
low
high
The Replacements
75.7
3
62
100
The Da Vinci Code
66.5
2
65
68
04
Distinct co-actors for Tom Hanks
Count unique people who share a movie with Tom Hanks. Without
DISTINCT, multi-film collaborations inflate the number.
CYPHER
MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m:Movie)
<-[:ACTED_IN]-(co:Person)
WHERE co <> tom
RETURNcount(co) AS pathRows,
count(DISTINCT co) AS uniqueCoActors
Expected result
pathRows counts every co-acting path (one row per shared movie).
uniqueCoActors counts each person once even if they share several films
with Tom Hanks—so path rows (39) exceed distinct co-actors (34).
pathRows
uniqueCoActors
39
34
Pitfalls
Returning a non-aggregated property “by accident”
If you write RETURN p.name, m.title, count(*), Cypher groups
by both name and title. You get one row per pair, and the count is usually 1. Either wrap
titles in collect or drop them from the projecting clause.
Counting paths instead of things
Pattern multiplicity is not a bug—it is the data. When the business question is about unique
people, movies, or relationships, put DISTINCT where the
identity matters, or collapse earlier with a careful WITH.
Nulls silently disappear from many aggregates
Missing ratings do not become zero. avg and
sum skip nulls; count(r.rating)
differs from count(*) when ratings are absent. Decide which
semantics product owners expect and encode them deliberately.
Aggregating after the variables you need are gone
A mid-query WITH p, count(m) AS c drops
m. Downstream clauses cannot reference movie titles unless
you also collect(m) or re-match. Plan the pipeline variables
before you type the first WITH.
Exercises
1.
Directors ranked by film count
Return each director’s name and the number of movies they directed, highest first. Use the
DIRECTED relationship.
STARTER
MATCH (p:Person)-[:DIRECTED]->(m:Movie)
// group by p.name and count moviesRETURN p.name
2.
Movies with more than three actors
Aggregate cast size, keep groups where the count is greater than three, and return title plus cast size.
STARTER
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
WITH m, count(p) AS castSize
// filter castSize and return
3.
Sum and average of release years for an actor
For Keanu Reeves, return the sum and average of m.released
across movies he acted in. Round the average to one decimal place.
STARTER
MATCH (p:Person {name: 'Keanu Reeves'})-[:ACTED_IN]->(m:Movie)
RETURN// sum(m.released), avg(...)
4.
Distinct collaborators for a director
Find people who acted in movies directed by Lilly Wachowski. Return the unique actor count
and a collected list of distinct names sorted alphabetically (hint: sort after collect using a list function or collect then post-process).