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
nullsorts 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, andf32compare 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.
orderByMultiplebreaks ties on each key with the next key.$idis a valid sort key. So are$distanceand$scoreuntil a step moves to another element (filter steps keep them). A search already returns hits in rank order.orderByafterbindfails withinvalid_after_bind_operation.
Bounds
limit,skip, andrangetake a non-negative literal or a barei64parameter. A negative parameter value fails the query.- A literal
rangewhose end is less than its start is rejected at planning; a parameter range like that returns no rows. - A
skiporrangestart 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, notorderByMultiple. - The traversal starts from a label-scoped source that filters the same property with
gt,gte,lt,lte, orbetween. Aneqwith a literal value qualifies when the property has a range index and no equality index. orderByfollows that source directly, with only filters in between.
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 onPost.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_atas the next page’s bound, usually as a typed parameter. - The bound must have the stored values’ type; comparisons across types are false.
ltskips 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.