RETURN and WITH — Projecting and Piping Results
Both clauses project rows forward. WITH hands the
projected rows to the next clause; RETURN ends the
query and hands them back to the caller. Rows typically come from a
preceding MATCH or UNWIND.
Think of
WITHas a pipe between stages, andRETURNas the output of the final stage.
Overview
| Goal | Clause |
|---|---|
| Shape the final output | RETURN |
| Rename a column | RETURN expr AS name |
| Deduplicate rows | DISTINCT |
| Sort / paginate | ORDER BY, SKIP, LIMIT |
| Build a subset-map per entity | Map projection |
| Conditional per-row value | CASE … WHEN … THEN … END |
| Pipe into the next stage | WITH |
| HAVING-style filtering | WITH … WHERE |
| Combine two result sets | UNION / UNION ALL |
RETURN
Basic projection
Return whole entities, bare properties, or any expression.
MATCH (n) RETURN n;
MATCH (n) RETURN n.name, n.age;
MATCH (n) RETURN n.name AS userName;
MATCH (n) RETURN n.age * 2 AS doubled_ageAliases (AS) set the column name in the host response. Reserve them
for anything the consumer has to look up by key.
Star
RETURN * projects every variable in scope. Handy for exploratory work,
noisy for production queries.
MATCH (a)-[r]->(b) RETURN *;
MATCH (a)-[r]->(b) RETURN *, a.name AS nameLiteral expressions
Return constants, function calls, arithmetic:
RETURN 1 + 2 AS three;
RETURN temporal.timestamp() AS now_ms;
RETURN temporal.now() AS now, temporal.today() AS today;
RETURN 'hello, ' + $name AS greetingDISTINCT
Deduplicate the output rows. Applies to the full row, not per-column.
MATCH (n) RETURN DISTINCT n.city;
MATCH (p:Person)-[:WROTE]->(:Post) RETURN DISTINCT pDISTINCT runs before ORDER BY and is expensive on
large inputs — prefer filtering with WHERE first.
ORDER BY, SKIP, LIMIT
Shape the final result set. Full reference: Ordering & Pagination.
MATCH (n) RETURN n ORDER BY n.name ASC;
MATCH (n) RETURN n ORDER BY n.last ASC, n.first DESC;
MATCH (n) RETURN n ORDER BY n.name DESC SKIP 5 LIMIT 10;
MATCH (n) RETURN n LIMIT 1Map projection
Shape a node or relationship into a map with only the keys you want — useful when the consumer doesn't need every property.
// Pick a subset
MATCH (n:User) RETURN n {.name, .age}
;// All properties (equivalent to \`properties(n)\`)
MATCH (n:User) RETURN n {.*}
;// Rename + compute
MATCH (n:User) RETURN n {.name, score: n.age * 2}
;// Include related data
MATCH (u:User)
RETURN u {.name, posts: [(u)-[:WROTE]->(p) | p.title]}See also Lists & Maps → Map projection.
CASE expressions
CASE is LoraDB's conditional expression — the Cypher equivalent of
SQL's CASE or a ternary. It's a plain expression, so it works
anywhere a value is allowed: RETURN, WITH, SET, ORDER BY, and
inside predicates.
Two forms.
Simple form — match an input against successive values:
MATCH (p:Product)
RETURN p.name,
CASE p.tier
WHEN 'gold' THEN 1.2
WHEN 'silver' THEN 1.1
WHEN 'bronze' THEN 1.0
ELSE 0.9
END AS multiplierGeneric form — each branch is its own boolean expression:
MATCH (o:Order)
RETURN o.id,
CASE
WHEN o.amount >= 1000 THEN 'large'
WHEN o.amount >= 100 THEN 'medium'
WHEN o.amount >= 10 THEN 'small'
ELSE 'tiny'
END AS bucketThe generic form is the one you'll reach for most often — it allows arbitrary predicates per branch, including null-safe checks and pattern-based predicates.
ELSE is optional
Omitting ELSE implicitly falls through to null:
MATCH (u:User)
RETURN u.name,
CASE WHEN u.score > 100 THEN 'pro' END AS tier
// tier is null for users at or below 100Branches are short-circuit
Branches evaluate top-to-bottom; the first matching WHEN wins. Place
the narrowest condition first if branches overlap.
Type coercion across branches
Every branch — including the implicit null from a missing ELSE —
can return any type. Nothing forces uniformity. Most callers prefer
one type per CASE for predictable downstream shape:
RETURN CASE WHEN $has_value THEN $value ELSE null END AS maybeIn predicates and filters
CASE is an expression, so it composes inside
WHERE and ORDER BY:
MATCH (p:Product)
WHERE CASE
WHEN p.on_sale THEN p.sale_price
ELSE p.price
END < $max
RETURN pMATCH (t:Task)
RETURN t
ORDER BY CASE t.status
WHEN 'urgent' THEN 0
WHEN 'open' THEN 1
ELSE 2
END, t.created_atThat ordering pattern is how you express "custom priority order" —
ASCII/byte order on the status string would give you open, urgent,
not what you want.
In SET and aggregates
MATCH (u:User)
SET u.tier = CASE WHEN u.score >= 100 THEN 'pro' ELSE 'free' ENDMATCH (r:Review)
RETURN r.product,
count(CASE WHEN r.stars >= 4 THEN 1 END) AS positive,
count(CASE WHEN r.stars <= 2 THEN 1 END) AS negativeCombining CASE with count(expr)
is the idiomatic way to express "count rows that satisfy X" inside a
larger aggregation — count skips null, so the missing ELSE
branch is exactly what you want.
See also
coalesce— a compact shorthand when you only need "first non-null".- WHERE → boolean operators — three-valued
logic rules that
CASEpredicates follow. - Ordering by computed expression.
WITH
WITH is the pipe of Cypher. Use it to split a query into stages. The
projected rows of one stage become the input rows of the next.
Piping variables
The simplest WITH — pass the bindings through untouched:
MATCH (a)-[r]->(b)
WITH a, r, b
RETURN a, r, bThat's pedagogical; a real query uses WITH to change something.
Transforming between stages
WITH can rename, compute, filter, aggregate — anything RETURN does
at the end of the pipeline.
MATCH (u:User)
WITH u, u.born AS year
WHERE year < 1900
RETURN u.name, yearHAVING-style filtering (WITH)
Aggregates are not allowed in WHERE. Aggregate into a
WITH, then filter:
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
WITH c.name AS company, count(p) AS employees
WHERE employees > 5
RETURN company, employeesSee Aggregation → HAVING-style filtering.
Renaming and shaping
MATCH (n:User)
WITH n.name AS username
RETURN usernameOrdering inside a pipeline
ORDER BY and LIMIT attach to a WITH stage just like they do to a
final RETURN. Only surviving rows move forward.
MATCH (n:User)
WITH n
ORDER BY n.age DESC
LIMIT 3
MATCH (n)-[:FOLLOWS]->(other)
RETURN n.name, other.nameChaining multiple WITH stages
MATCH (o:Order)-[:CONTAINS]->(i:Item)
WITH o, sum(i.price) AS total
WHERE total > 100
WITH o, total
ORDER BY total DESC
LIMIT 20
RETURN o.id, totalEach stage's output columns become the next stage's bindings — any variable not projected is dropped.
Losing variables through WITH
A variable must be explicitly projected into WITH to survive. This is
a common source of Unknown variable errors:
MATCH (a:User)-[r:KNOWS]->(b)
WITH a // r and b drop out of scope here
RETURN a, r // error: r is not in scopeEither pipe them through (WITH a, r, b) or don't bind them in the
first place.
UNION / UNION ALL
Combine two result sets that share a column shape. UNION
deduplicates; UNION ALL doesn't.
MATCH (n:User) RETURN n.name AS name
UNION
MATCH (n:Product) RETURN n.name AS nameMATCH (a:A) RETURN a.v AS v
UNION ALL
MATCH (b:B) RETURN b.v AS v
UNION ALL
MATCH (c:C) RETURN c.v AS vORDER BY / LIMIT across UNION
Apply at the very end — they shape the combined result:
MATCH (n:User) RETURN n.name AS name
UNION ALL
MATCH (n:Product) RETURN n.name AS name
ORDER BY name
LIMIT 10Column shape must match
Both sides must expose the same column names in the same order:
// Valid
MATCH (n:User) RETURN n.name AS name, 'user' AS kind
UNION ALL
MATCH (n:Product) RETURN n.name AS name, 'product' AS kind
;// Invalid — column shape mismatch
MATCH (n:User) RETURN n.name, 'user'
UNION
MATCH (n:Product) RETURN n.name, n.price, 'product'Common patterns
Pick N, then follow
Top-3 users by age, then project their friends:
MATCH (u:User)
WITH u ORDER BY u.age DESC LIMIT 3
MATCH (u)-[:FOLLOWS]->(f)
RETURN u.name, collect(f.name) AS followingCount-and-rank
MATCH (u:User)-[:WROTE]->(p:Post)
WITH u, count(p) AS posts
ORDER BY posts DESC
LIMIT 10
RETURN u.name, postsProject the top of a nested list
MATCH (p:Person)
RETURN p.name,
[(p)-[:KNOWS]->(f) | f.name][..5] AS first_five_friendsKeep only rows that meet an aggregate
MATCH (r:Review)
WITH r.product AS product, avg(r.stars) AS mean
WHERE mean >= 4.5
RETURN product, mean
ORDER BY mean DESCUsing both RETURN DISTINCT and ORDER BY
DISTINCT runs first — you can only order by projected columns.
MATCH (p:Person)
RETURN DISTINCT p.city AS city
ORDER BY cityEdge cases
Empty aggregation input
RETURN count(*) with zero matches still emits one row with value 0.
sum, avg, min, max return null on empty input. See
Aggregation → count.
WITH without projection
Every WITH must project at least one thing — there's no "pass
everything" shorthand. WITH * works and projects every in-scope
variable:
MATCH (a)-[r]->(b)
WITH *
RETURN a, r, bAggregation in WITH without a group key
Aggregating with no non-aggregated column folds everything into one row:
MATCH (o:Order)
WITH sum(o.amount) AS total
RETURN totalSee also
- MATCH — source of rows.
- WHERE — predicate filtering; also used after
WITH. - Aggregation — group-and-collapse semantics.
- Ordering & Pagination —
ORDER BY,SKIP,LIMIT. - Lists & Maps → Map projection.
- List Functions → Pattern comprehension — inline nested projections.