Skip to main content
Guide
Sort a stream with orderBy and cut it into pages with limit, skip, and range. Use this page when a response must come back in a stable order, one page at a time.

Operations

Order values: Order::Asc / Order::Desc (Rust), Order.Asc / Order.Desc (TypeScript), helix.OrderAsc / helix.OrderDesc (Go), Order.ASC / Order.DESC (Python).

Sort and page a stream

This request returns the second page of 20 posts, newest first. created_at holds epoch milliseconds:
range(20, 40) is the same as skip(20) followed by limit(20).

Ordering rules

These rules describe native queries.
  • A row without the sort property sorts first ascending and last descending. A stored null sorts after missing values and before every other value.
  • Mixed types sort by type: null, booleans, numbers, date-times, strings, bytes, arrays, then objects. i64, f64, and f32 compare by numeric value. Strings compare byte by byte, so the order is case-sensitive: "Zoe" sorts before "adam". Descending reverses the whole sequence.
  • Equal sort keys come back in ascending element ID order in both directions, so a repeated request returns the same order.
  • orderByMultiple breaks ties on each key with the next key.
  • $id is a valid sort key. So are $distance and $score until a step moves to another element (filter steps keep them). A search already returns hits in rank order.
  • orderBy after bind fails with invalid_after_bind_operation.

Bounds

  • limit, skip, and range take a non-negative literal or a bare i64 parameter. A negative parameter value fails the query.
  • A literal range whose end is less than its start is rejected at planning; a parameter range like that returns no rows.
  • A skip or range start past the end of the stream returns no rows.

Sorting cost

orderBy reads its whole input, holding every row in memory, before it returns the first row, even when a limit follows. A deep page costs a full sort: range(10000, 10020) sorts every row, then discards the first 10,000. A range index on the sort property removes the sort: rows come back in index order, and a following limit stops the scan early. A native query reads the index in order only when:
  • The sort uses one property: orderBy, not orderByMultiple.
  • The traversal starts from a label-scoped source that filters the same property with gt, gte, lt, lte, or between. An eq with a literal value qualifies when the property has a range index and no equality index.
  • orderBy follows that source directly, with only filters in between.
A native query reads an ascending index backwards for a descending sort, so one index serves both directions. Cypher and SQL read it only in its own direction; see Cypher indexes and performance.

Page with a range index

Keyset pagination asks for the rows after the last one the client saw instead of skipping rows. With an active range index on Post.created_at, this request stops once it has 20 rows, at any page depth. The limit bounds rows kept, not index entries read: a descending read resolves each group of equal created_at values whole, so many posts sharing one timestamp cost more reads:
  • Send the last row’s created_at as the next page’s bound, usually as a typed parameter.
  • The bound must have the stored values’ type; comparisons across types are false.
  • lt skips every row that ties with the bound, so sort on a unique property, or rows that share the last timestamp are lost between pages.

Next steps

Secondary indexes

Create the range index that serves ordered reads.

Filtering

Narrow the stream before you sort it.