ORDER BY, SKIP, LIMIT — Ordering and Pagination
ORDER BY, SKIP, and LIMIT shape the final result set of a
query, or the output of a WITH stage. They
are evaluated after projection and
aggregation.
Overview
| Goal | Clause |
|---|---|
| Sort ascending | ORDER BY expr ASC (default) |
| Sort descending | ORDER BY expr DESC |
| Sort by multiple keys | ORDER BY a ASC, b DESC |
| Skip rows | SKIP n |
| Limit rows | LIMIT n |
| Top-N | ORDER BY expr LIMIT n |
| Pagination | ORDER BY key SKIP $offset LIMIT $size |
Syntax
<RETURN | WITH> expr [, expr]
[ORDER BY expr [ASC | DESC] [, expr [ASC | DESC]]]
[SKIP n]
[LIMIT n]
n must be a non-negative integer literal or a parameter that resolves
to one. Negative or non-integer SKIP / LIMIT is a semantic error.
Order a single column
MATCH (n:User)
RETURN n.name
ORDER BY n.name ASCASC is the default; ORDER BY n.name is equivalent.
Direction comparison
| Type | Ordering |
|---|---|
Int, Float | Numeric (NaN is incomparable) |
String | Byte-lexicographic |
Boolean | false < true |
Date, DateTime, Time, LocalTime, LocalDateTime | Chronological |
Duration | By total length (calendar-aware) |
Point | Not orderable — equality only |
Null | See Nulls in ordering |
Multi-key ordering
Later keys break ties in earlier keys.
MATCH (p:Person)
RETURN p
ORDER BY p.last_name ASC, p.first_name ASC, p.id ASCMix directions freely:
MATCH (u:User)
RETURN u
ORDER BY u.country ASC, u.age DESCOrdering by computed expression
You can order on anything that evaluates to a comparable value.
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age * -1 DESC // youngest firstYou can also order by an alias defined in the same RETURN:
MATCH (u:User)-[:WROTE]->(:Post)
RETURN u.name AS author, count(*) AS posts
ORDER BY posts DESC, author ASCPagination — SKIP + LIMIT
MATCH (n:User)
RETURN n
ORDER BY n.id
SKIP 20
LIMIT 10SKIP 0/ noSKIP— start at the first row.LIMIT 0— return zero rows.LIMIT nwithoutORDER BY— the "first n" rows are undefined without a tiebreaker. Always pairLIMITwithORDER BYwhen the order matters.
Parameters work identically:
MATCH (n:User)
RETURN n
ORDER BY n.id
SKIP $offset
LIMIT $page_sizeStable pagination
SKIP + LIMIT is offset-based and can miss / repeat rows if the
underlying data changes between pages. For stable pagination, sort by an
immutable key and filter by "last seen":
MATCH (n:User)
WHERE n.id > $after
RETURN n
ORDER BY n.id
LIMIT $page_sizeThen use the last row's id as the next $after.
Ordering inside a pipeline
ORDER BY and LIMIT can attach to a WITH
stage. Only the surviving rows move forward.
MATCH (u:User)-[:WROTE]->(p:Post)
WITH u, count(p) AS posts
ORDER BY posts DESC
LIMIT 10
MATCH (u)-[:FOLLOWS]->(other)
RETURN u.name, count(other) AS followingThis is how you express "top 10 posters, then each of their followings".
Custom sort order with CASE
Use CASE to project a sort key
that doesn't match the data's natural ordering. Typical for ordering
strings by business meaning rather than alphabet:
MATCH (t:Task)
RETURN t.title, t.status
ORDER BY CASE t.status
WHEN 'urgent' THEN 0
WHEN 'open' THEN 1
WHEN 'review' THEN 2
ELSE 3
END, t.created_at DESCOne row per task, sorted urgent-first then newest-within-tier.
DISTINCT + ORDER BY
DISTINCT runs before ordering. A column used
to sort must either be a projected column or a deterministic expression
over projected columns.
MATCH (p:Person)
RETURN DISTINCT p.city
ORDER BY p.cityUNION + ORDER BY / LIMIT
For UNION / UNION ALL,
ORDER BY and LIMIT apply to 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 20Nulls in ordering
null values sort last in ascending order and first in
descending order. There is no NULLS FIRST / NULLS LAST keyword —
reverse the sort direction, or guard with
coalesce to
change placement.
// Nulls to the end of a DESC sort
MATCH (p:Person)
RETURN p.name, p.rank
ORDER BY coalesce(p.rank, -2147483648) DESC
;// Nulls to the start of an ASC sort
MATCH (p:Person)
RETURN p.name, p.rank
ORDER BY coalesce(p.rank, -2147483648) ASCCommon patterns
Top-N
MATCH (u:User)-[:WROTE]->(p:Post)
RETURN u.name, count(p) AS posts
ORDER BY posts DESC
LIMIT 10First row only
MATCH (u:User {email: $email})
RETURN u
ORDER BY u.created ASC
LIMIT 1Bottom-N (with tiebreaker)
MATCH (p:Product)
RETURN p
ORDER BY p.price ASC, p.id ASC
LIMIT 5Page N
MATCH (n:Post)
RETURN n
ORDER BY n.published_at DESC, n.id DESC
SKIP ($page - 1) * $size
LIMIT $sizeRandom sample (unstable)
MATCH (n)
RETURN n
ORDER BY math.random()
LIMIT 10math.random() is re-evaluated per row — good for
a rough sample, but don't rely on it for cryptographic randomness.
Edge cases
Ordering by a nullable column with NULL present
MATCH (p:Person)
RETURN p.name, p.rank
ORDER BY p.rank ASC
// Rows where p.rank IS NULL appear at the endOrdering by a type-mixed column
If p.score is sometimes Int and sometimes String, ordering is
well-defined but unlikely to match your intent. Cast with
toString or
toInteger first.
LIMIT in the middle of a pipeline
LIMIT on a WITH trims rows for downstream stages — subsequent
MATCH clauses only run for the surviving rows. Use it to keep a
multi-stage query bounded:
MATCH (u:User)
WITH u ORDER BY u.created DESC LIMIT 100
MATCH (u)-[:WROTE]->(p)
RETURN u.name, count(p)SKIP larger than result count
Returns zero rows — never an error.
Notes on performance
ORDER BYsorts the full projected result set in memory. Combine withLIMITwhen the input is large.ORDER BY n.propsorts the matched rows in memory. RANGE indexes can narrow predicates before the sort, but the current engine does not walk an index in sorted order to satisfyORDER BYdirectly.- Pair
LIMITwith a stable key (like an id) so re-running the same query yields the same rows.
See also
- RETURN / WITH — where ordering attaches.
- Aggregation — aggregation runs before ordering.
- Query Examples — copy-paste Top-N / pagination.
- Math → random — random sampling.
- Temporal Functions — ordering dates.