WHERE — Filtering Rows
WHERE filters rows produced by the preceding MATCH,
WITH, or
OPTIONAL MATCH. Any boolean expression
is valid.
WHEREruns beforeRETURNand aggregation. For filtering after an aggregate (SQLHAVING), pipe throughWITH.
Overview
| Goal | Operator / Keyword |
|---|---|
| Compare scalars | =, <>, <, <=, >, >= |
| Boolean combinators | AND, OR, NOT, XOR |
| Match a prefix / suffix / substring | STARTS WITH, ENDS WITH, CONTAINS |
| Regex | =~ |
| Null-safe check | IS NULL, IS NOT NULL |
| Membership | IN [...] / IN $param |
| List quantifiers | all, any, none, single |
| Pattern existence | EXISTS { (...)-[...]->(...) } |
| Conditional branch | CASE WHEN … THEN … END |
Comparison
MATCH (n:User) WHERE n.age > 18 RETURN n
MATCH (n:User) WHERE n.age >= 18 AND n.age <= 65 RETURN n
MATCH (n:User) WHERE n.name = 'alice' RETURN n
MATCH (n:User) WHERE n.name <> 'bob' RETURN n
Comparison returns null (not false) when either operand is null or
when the types mismatch — see Scalars → Null
and Limitations.
No BETWEEN keyword — use explicit >= / <= bounds:
MATCH (p:Product) WHERE p.price >= 10 AND p.price <= 50 RETURN p
Boolean operators
MATCH (n) WHERE n.active AND n.age >= 18 RETURN n
MATCH (n) WHERE n.active OR n.age < 18 RETURN n
MATCH (n) WHERE NOT n.active RETURN n
MATCH (n) WHERE n.active XOR n.admin RETURN n
Three-valued logic applies — null AND false is false, null AND true is null. See the full truth table in
Scalars → Null.
Precedence
NOT binds tightest, then AND, then XOR, then OR. Parenthesise
freely when in doubt:
-- These are equivalent
MATCH (n) WHERE n.a OR n.b AND n.c RETURN n
MATCH (n) WHERE n.a OR (n.b AND n.c) RETURN n
String matching
All string operators are case-sensitive. For case-insensitive
matching, normalise with toLower
or toUpper on both sides.
MATCH (n) WHERE n.name STARTS WITH 'a' RETURN n
MATCH (n) WHERE n.name ENDS WITH 'z' RETURN n
MATCH (n) WHERE n.name CONTAINS 'al' RETURN n
MATCH (u:User)
WHERE toLower(u.name) STARTS WITH toLower($query)
RETURN u
Regex
MATCH (u:User) WHERE u.name =~ 'A.*e' RETURN u
MATCH (u:User) WHERE u.email =~ '.*@loradb\\.com' RETURN u
Uses the Rust regex crate — standard RE2-style syntax, no
backreferences. Anchors are implicit: =~ 'foo' matches only the full
string "foo", not any string containing foo. Use .* to allow
prefixes/suffixes, or CONTAINS 'foo' for plain substring.
Null checks
Most expressions involving null propagate to null, not to false.
Use IS NULL / IS NOT NULL, not = null.
MATCH (n) WHERE n.optional IS NULL RETURN n
MATCH (n) WHERE n.optional IS NOT NULL RETURN n
-- Wrong — always yields zero rows
MATCH (n) WHERE n.optional = null RETURN n
Common guard: require a property to exist and be non-empty:
MATCH (u:User)
WHERE u.email IS NOT NULL AND size(u.email) > 0
RETURN u
IN
Membership check against a list literal or parameter.
MATCH (n) WHERE n.age IN [18, 21, 25] RETURN n
MATCH (n) WHERE NOT n.name IN ['Alice', 'Bob'] RETURN n
MATCH (u:User) WHERE u.id IN $ids RETURN u
$ids binds to a list in the host
language ([1, 2, 3] in JS/Python, Vec<LoraValue> in Rust).
IN with DISTINCT
MATCH (u:User)-[:OWNS]->(p:Project)
WHERE p.tag IN $tags
RETURN DISTINCT u
IN over an empty list
x IN [] is always false. Empty-list parameters drop every row —
validate on the host side if that's a likely accident.
Arithmetic in WHERE
Any expression that produces a boolean is allowed.
MATCH (n) WHERE n.age + 5 > 30 RETURN n
MATCH (n) WHERE n.price * n.quantity > 1000 RETURN n
MATCH (n) WHERE (n.end - n.start).seconds > 60 RETURN n
Cross-variable comparison
Predicates can reference multiple bindings from the MATCH:
MATCH (a:User)-[:FOLLOWS]->(b:User)
WHERE a.age > b.age
RETURN a.name AS older, b.name AS younger
MATCH (a:User)-[:FOLLOWS]->(b)
WHERE a.country = b.country
RETURN a, b
Pattern existence
Use EXISTS { pattern } to filter rows by whether a pattern matches —
without adding extra rows to the output.
MATCH (u:User)
WHERE EXISTS { (u)-[:FOLLOWS]->() }
RETURN u
-- Users who have never posted
MATCH (u:User)
WHERE NOT EXISTS { (u)-[:WROTE]->(:Post) }
RETURN u.name
This is the anti-join pattern — cheaper than
OPTIONAL MATCH … WHERE other IS NULL when you don't need the optional
result.
List predicates
Ask a question about the elements of a list. Covered fully in List Functions → Predicates.
MATCH (n) WHERE all(x IN n.scores WHERE x > 0) RETURN n
MATCH (n) WHERE any(x IN n.tags WHERE x = 'VIP') RETURN n
MATCH (n) WHERE none(x IN n.scores WHERE x < 0) RETURN n
MATCH (n) WHERE single(x IN n.scores WHERE x = 100) RETURN n
CASE in predicates
CASE is an expression, so it
composes inside WHERE wherever you'd write a scalar. Useful when the
comparison value itself depends on a per-row condition:
MATCH (p:Product)
WHERE CASE
WHEN p.on_sale THEN p.sale_price
ELSE p.price
END <= $budget
RETURN p
Equivalent with coalesce
when you only need a "first non-null" fallback:
MATCH (p:Product)
WHERE coalesce(p.sale_price, p.price) <= $budget
RETURN p
See RETURN → CASE expressions for
the full syntax.
Common patterns
Safe prefix search
MATCH (u:User)
WHERE toLower(u.name) STARTS WITH toLower($query)
RETURN u
ORDER BY u.name
LIMIT 20
Tag filtering
MATCH (p:Product)
WHERE any(t IN p.tags WHERE t IN $tags)
RETURN p
Date range
MATCH (e:Event)
WHERE e.at >= date('2024-01-01') AND e.at < date('2025-01-01')
RETURN e
ORDER BY e.at
See Temporal Functions for constructors and arithmetic.
"Has at least one of each"
MATCH (u:User)
WHERE EXISTS { (u)-[:OWNS]->(:Repo) }
AND EXISTS { (u)-[:WROTE]->(:Post) }
RETURN u
"Has none of these"
MATCH (p:Post)
WHERE none(t IN ['spam', 'nsfw', 'flagged']
WHERE EXISTS { (p)-[:TAGGED]->(:Tag {name: t}) })
RETURN p
Chained optional predicates
Break a complex predicate into WITH stages for readability. Each
stage only sees what it needs:
MATCH (u:User)
WITH u, coalesce(u.score, 0) AS s
WHERE s >= 50
WITH u, s, (u.last_seen >= datetime() - duration('P30D')) AS recent
WHERE recent
RETURN u.handle, s
Same meaning as one giant WHERE, but each stage is narrower and
easier to trace.
Default a missing value
Use coalesce to
substitute a fallback:
MATCH (p:Person)
WHERE coalesce(p.score, 0) >= 50
RETURN p
Edge cases
Aggregates in WHERE
Aggregates are not allowed in WHERE. Attempting WHERE count(x) > 5
is a semantic error — use
WITH … WHERE
instead.
Missing property vs null property
A missing property and a property set to null are indistinguishable —
both return null on access. SET n.prop = null is the idiomatic way
to remove a property (see SET → computed expressions).
Comparison across types
Cross-type comparisons (e.g. Int < String) return null, not false
— see Limitations. Cast
explicitly with toString /
toInteger / toFloat first.
null propagation in compound predicates
-- If n.a is null, the whole expression is null → row dropped
MATCH (n) WHERE n.a = 1 AND n.b > 2 RETURN n
Guard null-prone properties with IS NOT NULL or coalesce when you
need to reason about three-valued logic.
See also
- MATCH — what feeds
WHERE. - RETURN / WITH — projection and HAVING-style filtering.
- Aggregation — group, then filter via
WITH. - String Functions —
toLower,replace, regex. - List Functions —
all,any,none,single. - Scalars → Null — three-valued logic.
- Temporal Functions — date/time predicates.