Skip to main content

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

GoalClause
Sort ascendingORDER BY expr ASC (default)
Sort descendingORDER BY expr DESC
Sort by multiple keysORDER BY a ASC, b DESC
Skip rowsSKIP n
Limit rowsLIMIT n
Top-NORDER BY expr LIMIT n
PaginationORDER 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 ASC

ASC is the default; ORDER BY n.name is equivalent.

Direction comparison

TypeOrdering
Int, FloatNumeric (NaN is incomparable)
StringByte-lexicographic
Booleanfalse < true
Date, DateTime, Time, LocalTime, LocalDateTimeChronological
DurationBy total length (calendar-aware)
PointNot orderable — equality only
NullSee 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 ASC

Mix directions freely:

MATCH (u:User)
RETURN u
ORDER BY u.country ASC, u.age DESC

Ordering 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 first

You 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 ASC

Pagination — SKIP + LIMIT

MATCH (n:User)
RETURN n
ORDER BY n.id
SKIP 20
LIMIT 10
  • SKIP 0 / no SKIP — start at the first row.
  • LIMIT 0 — return zero rows.
  • LIMIT n without ORDER BY — the "first n" rows are undefined without a tiebreaker. Always pair LIMIT with ORDER BY when the order matters.

Parameters work identically:

MATCH (n:User)
RETURN n
ORDER BY n.id
SKIP $offset
LIMIT $page_size

Stable 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_size

Then 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 following

This 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 DESC

One 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.city

UNION + 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 20

Nulls 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) ASC

Common patterns

Top-N

MATCH (u:User)-[:WROTE]->(p:Post)
RETURN u.name, count(p) AS posts
ORDER BY posts DESC
LIMIT 10

First row only

MATCH (u:User {email: $email})
RETURN u
ORDER BY u.created ASC
LIMIT 1

Bottom-N (with tiebreaker)

MATCH (p:Product)
RETURN p
ORDER BY p.price ASC, p.id ASC
LIMIT 5

Page N

MATCH (n:Post)
RETURN n
ORDER BY n.published_at DESC, n.id DESC
SKIP ($page - 1) * $size
LIMIT $size

Random sample (unstable)

MATCH (n)
RETURN n
ORDER BY rand()
LIMIT 10

rand() 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 end

Ordering 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 BY sorts the full projected result set in memory. Combine with LIMIT when the input is large.
  • There are no property indexes (see Limitations), so ORDER BY n.prop walks every matched row, not a pre-sorted index.
  • Pair LIMIT with a stable key (like an id) so re-running the same query yields the same rows.

See also