> ## Documentation Index
> Fetch the complete documentation index at: https://docs.helix-db.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ordering and pagination

> Sort traversal streams and page through them with limit, skip, range, and range indexes

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

| Intent | Rust and Python | TypeScript | Go |
| - | - | - | - |
| Sort by one property | `order_by(property, order)` | `orderBy` | `OrderBy` |
| Sort by several properties | `order_by_multiple` with a list of `(property, order)` pairs | `orderByMultiple` | `OrderByMultiple(helix.Ordering{...}, ...)` |
| Keep the first `n` rows | `limit(n)` | `limit` | `Limit` |
| Drop the first `n` rows | `skip(n)` | `skip` | `Skip` |
| Keep rows from `start` up to, but not including, `end` | `range(start, end)` | `range` | `Range` |

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:

```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
read_batch()
    .var_as(
        "posts",
        g()
            .n_with_label("Post")
            .order_by("created_at", Order::Desc)
            .range(20, 40)
            .value_map(Some(vec!["$id", "title", "created_at"])),
    )
    .returning(["posts"]);
```

```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
readBatch()
  .varAs(
    "posts",
    g()
      .nWithLabel("Post")
      .orderBy("created_at", Order.Desc)
      .range(20, 40)
      .valueMap(["$id", "title", "created_at"]),
  )
  .returning(["posts"]);
```

```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
helix.ReadQuery("recent_posts").
	VarAs(
		"posts",
		helix.G().
			NWithLabel("Post").
			OrderBy("created_at", helix.OrderDesc).
			Range(20, 40).
			ValueMap("$id", "title", "created_at"),
	).
	Returning("posts")
```

```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
(
    read_batch()
    .var_as(
        "posts",
        g()
        .n_with_label("Post")
        .order_by("created_at", Order.DESC)
        .range(20, 40)
        .value_map(["$id", "title", "created_at"]),
    )
    .returning(["posts"])
)
```

```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "request_type": "read",
  "query_name": "recent_posts",
  "query": {
    "read": {
      "entries": [{
        "query": {
          "name": "posts",
          "root": {
            "value_map": {
              "input": {
                "range": {
                  "input": {
                    "order_by": {
                      "input": {
                        "nodes_where": {
                          "predicate": {
                            "eq": {
                              "left": { "property": "$label" },
                              "right": { "constant": { "string": "Post" } }
                            }
                          }
                        }
                      },
                      "property": "created_at",
                      "order": "desc"
                    }
                  },
                  "start": { "literal": 20 },
                  "end": { "literal": 40 }
                }
              },
              "properties": ["$id", "title", "created_at"]
            }
          }
        }
      }],
      "returns": ["posts"]
    }
  }
}
```

```cypher Cypher theme={"languages":{"custom":["languages/helixql.json"]}}
MATCH (p:Post)
RETURN id(p) AS id, p.title AS title, p.created_at AS created_at
ORDER BY created_at DESC
SKIP 20 LIMIT 20
```

```sql SQL theme={"languages":{"custom":["languages/helixql.json"]}}
SELECT id(p) AS id, p.title AS title, p.created_at AS created_at
FROM Post p
ORDER BY created_at DESC
LIMIT 20 OFFSET 20
```

`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](https://docs.helix-db.com/database/helix-db/query-guides/secondary-indexes.md#range-indexes) 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](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#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:

```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
read_batch()
    .var_as(
        "posts",
        g()
            .n_with_label_where(
                "Post",
                SourcePredicate::lt("created_at", 1_793_525_400_000_i64),
            )
            .order_by("created_at", Order::Desc)
            .limit(20)
            .value_map(Some(vec!["$id", "title", "created_at"])),
    )
    .returning(["posts"]);
```

```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
readBatch()
  .varAs(
    "posts",
    g()
      .nWithLabelWhere("Post", SourcePredicate.lt("created_at", 1793525400000))
      .orderBy("created_at", Order.Desc)
      .limit(20)
      .valueMap(["$id", "title", "created_at"]),
  )
  .returning(["posts"]);
```

```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
helix.ReadQuery("posts_before").
	VarAs(
		"posts",
		helix.G().
			NWithLabelWhere("Post", helix.SourceLt("created_at", int64(1793525400000))).
			OrderBy("created_at", helix.OrderDesc).
			Limit(20).
			ValueMap("$id", "title", "created_at"),
	).
	Returning("posts")
```

```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
(
    read_batch()
    .var_as(
        "posts",
        g()
        .n_with_label_where(
            "Post", SourcePredicate.lt("created_at", 1793525400000)
        )
        .order_by("created_at", Order.DESC)
        .limit(20)
        .value_map(["$id", "title", "created_at"]),
    )
    .returning(["posts"])
)
```

```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "request_type": "read",
  "query_name": "posts_before",
  "query": {
    "read": {
      "entries": [{
        "query": {
          "name": "posts",
          "root": {
            "value_map": {
              "input": {
                "limit": {
                  "input": {
                    "order_by": {
                      "input": {
                        "nodes_where": {
                          "predicate": {
                            "and": {
                              "predicates": [
                                {
                                  "eq": {
                                    "left": { "property": "$label" },
                                    "right": { "constant": { "string": "Post" } }
                                  }
                                },
                                {
                                  "lt": {
                                    "left": { "property": "created_at" },
                                    "right": { "constant": { "i64": 1793525400000 } }
                                  }
                                }
                              ]
                            }
                          }
                        }
                      },
                      "property": "created_at",
                      "order": "desc"
                    }
                  },
                  "count": { "literal": 20 }
                }
              },
              "properties": ["$id", "title", "created_at"]
            }
          }
        }
      }],
      "returns": ["posts"]
    }
  }
}
```

* Send the last row's `created_at` as the next page's bound, usually as a typed
  [parameter](https://docs.helix-db.com/database/helix-db/query-guides/parameters.md).
* 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

> [card] [Secondary indexes](https://docs.helix-db.com/database/helix-db/query-guides/secondary-indexes.md)
>
> Create the range index that serves ordered reads.

> [card] [Filtering](https://docs.helix-db.com/database/helix-db/query-guides/filtering.md)
>
> Narrow the stream before you sort it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.