Pagination
Pagination refers to the process of splitting large result sets into smaller "pages". Doing so relieves the server, and the client, from dealing with potentially huge result sets in one step. The tradeoff is that more work needs to be done if you do need to process large result sets.
Currently, when you want to process all of the documents in a collection, for searching, sorting, or simply fetching one or more fields, an index is typically required (the only exception would be when you would track each document’s Ref independently, in your code).
This tutorial demonstrates several pagination scenarios:
This tutorial assumes that you have successfully prepared your database by creating the necessary collections and documents. |
Basic pagination
The easiest way to demonstrate pagination is to use a "collection index",
this is, an index that has no defined terms
or values
fields. Such
indexes record each member document’s Ref.
That makes collection indexes useful for retrieving all of the
collection’s documents.
If you are using the Shell in the FaunaDB Console, the Console
automatically created a collection index called all_letters
. If you
are using fauna-shell
, you need to create the index. Copy the
following query, paste it into the Shell, and run it:
CreateIndex({
name: "all_letters",
source: Collection("Letters")
})
There are several different ways that we can use collection indexes to fetch documents. For the following examples, copy a query and paste it into the Shell, and run it.
All documents, for small collections
Our Letters
collection contains 26 documents, one for each letter in the
alphabet. We can fetch them all in one query, using the index that we
created in the previous section:
Paginate(Match(Index("all_letters")))
This query uses the Index
function
to specify that we’re working with the all_letters
index. The
Match
function is used to "search"
the index, producing a set of results; no search criteria is provided
because the index has no terms
specified with which to search. The
Paginate
function is used to
traverse the result set’s entries.
When you run this query, the result should look similar to:
{
"data": [
Ref(Collection("Letters"), "101"),
Ref(Collection("Letters"), "102"),
Ref(Collection("Letters"), "103"),
Ref(Collection("Letters"), "104"),
Ref(Collection("Letters"), "105"),
Ref(Collection("Letters"), "106"),
Ref(Collection("Letters"), "107"),
Ref(Collection("Letters"), "108"),
Ref(Collection("Letters"), "109"),
Ref(Collection("Letters"), "110"),
Ref(Collection("Letters"), "111"),
Ref(Collection("Letters"), "112"),
Ref(Collection("Letters"), "113"),
Ref(Collection("Letters"), "114"),
Ref(Collection("Letters"), "115"),
Ref(Collection("Letters"), "116"),
Ref(Collection("Letters"), "117"),
Ref(Collection("Letters"), "118"),
Ref(Collection("Letters"), "119"),
Ref(Collection("Letters"), "120"),
Ref(Collection("Letters"), "121"),
Ref(Collection("Letters"), "122"),
Ref(Collection("Letters"), "123"),
Ref(Collection("Letters"), "124"),
Ref(Collection("Letters"), "125"),
Ref(Collection("Letters"), "126")
]
}
The result demonstrates that the index contains references to the documents. These references are useful, we can pick one to fetch the details for a specific document.
Get(Ref(Collection("Letters"), "103"))
When that query is executed, the result should be similar to:
{
"ref": Ref(Collection("Letters"), "103"),
"ts": 1565299176485000,
"data": {
"letter": "C",
"extra": "third"
}
}
What if we wanted to see the contents of every document, and not just
the refs? That’s where pagination is needed. We would change our
Paginate
query to look like this:
Map(
Paginate(Match(Index("all_letters"))),
Lambda("X", Get(Var("X")))
)
The points of interest for this query:
-
We use the
Map
function to process each of thePaginate
results with aLambda
function. -
We use the variable named
X
, just to provide a variable name viaVar
. You can use any variable name you like. -
We only need one variable for the
Lambda
function, because our index only returns one value for each document in the collection: the document’s Ref. If our index specified avalues
field, we would need to use one variable per field defined by thevalues
field. -
The
Lambda
function simply calls theGet
function to retrieve the document specified by theX
variable.
When you run this query, the result should be similar to:
{
"data": [
{
"ref": Ref(Collection("Letters"), "101"),
"ts": 1565299176485000,
"data": {
"letter": "A",
"extra": "First"
}
},
{
"ref": Ref(Collection("Letters"), "102"),
"ts": 1565299176485000,
"data": {
"letter": "B",
"extra": "second"
}
},
{
"ref": Ref(Collection("Letters"), "103"),
"ts": 1565299176485000,
"data": {
"letter": "C",
"extra": "third"
}
},
{
"ref": Ref(Collection("Letters"), "104"),
"ts": 1565299176485000,
"data": {
"letter": "D",
"extra": "4th"
}
},
{
"ref": Ref(Collection("Letters"), "105"),
"ts": 1565299176485000,
"data": {
"letter": "E",
"extra": "fifth"
}
},
{
"ref": Ref(Collection("Letters"), "106"),
"ts": 1565299176485000,
"data": {
"letter": "F",
"extra": "sixth"
}
},
{
"ref": Ref(Collection("Letters"), "107"),
"ts": 1565299176485000,
"data": {
"letter": "G",
"extra": "seventh"
}
},
{
"ref": Ref(Collection("Letters"), "108"),
"ts": 1565299176485000,
"data": {
"letter": "H",
"extra": "eighth"
}
},
{
"ref": Ref(Collection("Letters"), "109"),
"ts": 1565299176485000,
"data": {
"letter": "I",
"extra": "9th"
}
},
{
"ref": Ref(Collection("Letters"), "110"),
"ts": 1565299176485000,
"data": {
"letter": "J",
"extra": "tenth"
}
},
{
"ref": Ref(Collection("Letters"), "111"),
"ts": 1565299176485000,
"data": {
"letter": "K",
"extra": 11
}
},
{
"ref": Ref(Collection("Letters"), "112"),
"ts": 1565299176485000,
"data": {
"letter": "L",
"extra": ""
}
},
{
"ref": Ref(Collection("Letters"), "113"),
"ts": 1565299176485000,
"data": {
"letter": "M"
}
},
{
"ref": Ref(Collection("Letters"), "114"),
"ts": 1565299176485000,
"data": {
"letter": "N",
"extra": "14th"
}
},
{
"ref": Ref(Collection("Letters"), "115"),
"ts": 1565299176485000,
"data": {
"letter": "O",
"extra": "fifteenth"
}
},
{
"ref": Ref(Collection("Letters"), "116"),
"ts": 1565299176485000,
"data": {
"letter": "P",
"extra": "16th"
}
},
{
"ref": Ref(Collection("Letters"), "117"),
"ts": 1565299176485000,
"data": {
"letter": "Q",
"extra": "seventeenth"
}
},
{
"ref": Ref(Collection("Letters"), "118"),
"ts": 1565299176485000,
"data": {
"letter": "R",
"extra": "18th"
}
},
{
"ref": Ref(Collection("Letters"), "119"),
"ts": 1565299176485000,
"data": {
"letter": "S",
"extra": "19th"
}
},
{
"ref": Ref(Collection("Letters"), "120"),
"ts": 1565299176485000,
"data": {
"letter": "T",
"extra": "20th"
}
},
{
"ref": Ref(Collection("Letters"), "121"),
"ts": 1565299176485000,
"data": {
"letter": "U",
"extra": "21st"
}
},
{
"ref": Ref(Collection("Letters"), "122"),
"ts": 1565299176485000,
"data": {
"letter": "V",
"extra": "22nd"
}
},
{
"ref": Ref(Collection("Letters"), "123"),
"ts": 1565299176485000,
"data": {
"letter": "W",
"extra": "twenty-third"
}
},
{
"ref": Ref(Collection("Letters"), "124"),
"ts": 1565299176485000,
"data": {
"letter": "X",
"extra": 24
}
},
{
"ref": Ref(Collection("Letters"), "125"),
"ts": 1565299176485000,
"data": {
"letter": "Y",
"extra": "24 + 1"
}
},
{
"ref": Ref(Collection("Letters"), "126"),
"ts": 1565299176485000,
"data": {
"letter": "Z"
}
}
]
}
Note that the results are in sorted order. That’s because we provided
specific ids for each Letters
document rather than using FaunaDB’s
auto-generated ids. When multiple documents are created in a single
transaction, FaunaDB opportunistically runs portions of the
transaction’s queries in parallel. This can result in seemingly random
document ids that can appear to sort documents chaotically.
For example, let’s query the People
documents to see their order. Copy
the following query, paste it into the Shell, and run it:
Map(
Paginate(Match(Index("all_people"))),
Lambda("X", Get(Var("X")))
)
When you run the query, the output should be similar to:
{
"data": [
{
"ref": Ref(Collection("People"), "240166254282801673"),
"ts": 1565299238420000,
"data": {
"first": "Alan",
"last": "Turing",
"letter": "B"
}
},
{
"ref": Ref(Collection("People"), "240166254282802697"),
"ts": 1565299238420000,
"data": {
"first": "Tim",
"last": "Cook",
"letter": "G"
}
},
{
"ref": Ref(Collection("People"), "240166254282803721"),
"ts": 1565299238420000,
"data": {
"first": "Leslie",
"last": "Lamport"
}
},
{
"ref": Ref(Collection("People"), "240166254282804745"),
"ts": 1565299238420000,
"data": {
"first": "Marvin",
"last": "Minsky",
"letter": 1
}
},
{
"ref": Ref(Collection("People"), "240166254282805769"),
"ts": 1565299238420000,
"data": {
"first": "Alan",
"last": "Perlis",
"letter": "A"
}
},
{
"ref": Ref(Collection("People"), "240166254282806793"),
"ts": 1565299238420000,
"data": {
"first": "Grace",
"last": "Hopper",
"letter": "C"
}
},
{
"ref": Ref(Collection("People"), "240166254282807817"),
"ts": 1565299238420000,
"data": {
"first": "Stephen",
"last": "Cook",
"letter": "F"
}
}
]
}
Follow the sort tutorial to see how to return the documents in order.
All documents, for large collections
The Paginate
function defaults to returning up to 64 documents in a
"page", which is a subset of the results.
Paginate
works this way to prevent a common problem encountered with
SQL databases when a user executes a query such as SELECT * FROM TABLE
x;
: the user doesn’t know how large each record might be, nor how many
records might exist. This could result in fetching exceedingly large
records, exceedingly large numbers of records, or both. These scenarios
tax the database server, and the client code, consuming resources to
store, transmit, and report results, and can often result in database
server outages and/or client non-responsiveness.
So, what happens when you have more documents than Paginate
would
return? First, the default page size of 64 can be adjusted, up to the
maximum value of 100,000 documents. If your collection has fewer
documents than the maximum, you could return all of a collection’s
documents in a single query. For collections with more than 100,000
documents, multiple queries are required.
We can simulate what happens by making Paginate
's page size smaller
by specifying the size
parameter:
Map(
Paginate(Match(Index("all_letters")), { size: 3 }),
Lambda("X", Get(Var("X")))
)
Note that we’re using the collection index that we created in the Basic pagination section.
When you run this query, the results should be similar to:
{
"after": [
Ref(Collection("Letters"), "104")
],
"data": [
{
"ref": Ref(Collection("Letters"), "101"),
"ts": 1565299176485000,
"data": {
"letter": "A",
"extra": "First"
}
},
{
"ref": Ref(Collection("Letters"), "102"),
"ts": 1565299176485000,
"data": {
"letter": "B",
"extra": "second"
}
},
{
"ref": Ref(Collection("Letters"), "103"),
"ts": 1565299176485000,
"data": {
"letter": "C",
"extra": "third"
}
}
]
}
These results show that our query returned the first 3 documents, in the same order as the previous results.
Notice the inclusion of the after
field. This is a cursor, a marker
the points to the entry that would start the next page of results. The
cursor’s structure is determined by the values
field of the index,
which for our all_letters
index is just each document’s Ref.
If we wanted to fetch the next page of results, we need to specify
after
in the Paginate
function call:
Map(
Paginate(
Match(Index("all_letters")),
{
size: 3,
after: [ Ref(Collection("Letters"), "104") ]
}
),
Lambda("X", Get(Var("X")))
)
When you run this query, the results should be similar to:
{
"before": [
Ref(Collection("Letters"), "104")
],
"after": [
Ref(Collection("Letters"), "107")
],
"data": [
{
"ref": Ref(Collection("Letters"), "104"),
"ts": 1565299176485000,
"data": {
"letter": "D",
"extra": "4th"
}
},
{
"ref": Ref(Collection("Letters"), "105"),
"ts": 1565299176485000,
"data": {
"letter": "E",
"extra": "fifth"
}
},
{
"ref": Ref(Collection("Letters"), "106"),
"ts": 1565299176485000,
"data": {
"letter": "F",
"extra": "sixth"
}
}
]
}
As you can see, we now have the next group of 3 documents from our document collection.
Notice that, this time, there is now a before
field in the results.
Like after
, before
is a cursor that points to the first entry in the
current page, so that FaunaDB can use the size
parameter to determine
which results to include in the previous page.
The size
parameter can be different sizes in different queries. If we
repeat the previous query with size
adjust to 1:
Map(
Paginate(
Match(Index("all_letters")),
{
size: 1,
after: [ Ref(Collection("Letters"), "104") ]
}
),
Lambda("X", Get(Var("X")))
)
the result would be similar to:
{
"before": [
Ref(Collection("Letters"), "104")
],
"after": [
Ref(Collection("Letters"), "105")
],
"data": [
{
"ref": Ref(Collection("Letters"), "104"),
"ts": 1565299176485000,
"data": {
"letter": "D",
"extra": "4th"
}
}
]
}
So, now you might be wondering, "what happens when we run out of
pages?". Let’s run the previous query again, but adjust the size
parameter to 100 (far more than the number of People
documents in our
collection):
Map(
Paginate(
Match(Index("all_letters")),
{
size: 100,
after: [ Ref(Collection("Letters"), "104") ]
}
),
Lambda("X", Get(Var("X")))
)
The result should be similar to:
{
"before": [
Ref(Collection("Letters"), "104")
],
"data": [
{
"ref": Ref(Collection("Letters"), "104"),
"ts": 1565299176485000,
"data": {
"letter": "D",
"extra": "4th"
}
},
{
"ref": Ref(Collection("Letters"), "105"),
"ts": 1565299176485000,
"data": {
"letter": "E",
"extra": "fifth"
}
},
{
"ref": Ref(Collection("Letters"), "106"),
"ts": 1565299176485000,
"data": {
"letter": "F",
"extra": "sixth"
}
},
{
"ref": Ref(Collection("Letters"), "107"),
"ts": 1565299176485000,
"data": {
"letter": "G",
"extra": "seventh"
}
},
{
"ref": Ref(Collection("Letters"), "108"),
"ts": 1565299176485000,
"data": {
"letter": "H",
"extra": "eighth"
}
},
{
"ref": Ref(Collection("Letters"), "109"),
"ts": 1565299176485000,
"data": {
"letter": "I",
"extra": "9th"
}
},
{
"ref": Ref(Collection("Letters"), "110"),
"ts": 1565299176485000,
"data": {
"letter": "J",
"extra": "tenth"
}
},
{
"ref": Ref(Collection("Letters"), "111"),
"ts": 1565299176485000,
"data": {
"letter": "K",
"extra": 11
}
},
{
"ref": Ref(Collection("Letters"), "112"),
"ts": 1565299176485000,
"data": {
"letter": "L",
"extra": ""
}
},
{
"ref": Ref(Collection("Letters"), "113"),
"ts": 1565299176485000,
"data": {
"letter": "M"
}
},
{
"ref": Ref(Collection("Letters"), "114"),
"ts": 1565299176485000,
"data": {
"letter": "N",
"extra": "14th"
}
},
{
"ref": Ref(Collection("Letters"), "115"),
"ts": 1565299176485000,
"data": {
"letter": "O",
"extra": "fifteenth"
}
},
{
"ref": Ref(Collection("Letters"), "116"),
"ts": 1565299176485000,
"data": {
"letter": "P",
"extra": "16th"
}
},
{
"ref": Ref(Collection("Letters"), "117"),
"ts": 1565299176485000,
"data": {
"letter": "Q",
"extra": "seventeenth"
}
},
{
"ref": Ref(Collection("Letters"), "118"),
"ts": 1565299176485000,
"data": {
"letter": "R",
"extra": "18th"
}
},
{
"ref": Ref(Collection("Letters"), "119"),
"ts": 1565299176485000,
"data": {
"letter": "S",
"extra": "19th"
}
},
{
"ref": Ref(Collection("Letters"), "120"),
"ts": 1565299176485000,
"data": {
"letter": "T",
"extra": "20th"
}
},
{
"ref": Ref(Collection("Letters"), "121"),
"ts": 1565299176485000,
"data": {
"letter": "U",
"extra": "21st"
}
},
{
"ref": Ref(Collection("Letters"), "122"),
"ts": 1565299176485000,
"data": {
"letter": "V",
"extra": "22nd"
}
},
{
"ref": Ref(Collection("Letters"), "123"),
"ts": 1565299176485000,
"data": {
"letter": "W",
"extra": "twenty-third"
}
},
{
"ref": Ref(Collection("Letters"), "124"),
"ts": 1565299176485000,
"data": {
"letter": "X",
"extra": 24
}
},
{
"ref": Ref(Collection("Letters"), "125"),
"ts": 1565299176485000,
"data": {
"letter": "Y",
"extra": "24 + 1"
}
},
{
"ref": Ref(Collection("Letters"), "126"),
"ts": 1565299176485000,
"data": {
"letter": "Z"
}
}
]
}
We have received all of the people after the first 3. Notice that the
after
field is no longer present. When after
is not included, that
means that there are no pages after these results. When before
is not
included, there are no pages before these results. That makes it easy
for application code to determine when to stop issuing queries when
fetching lots of records.
Paginating indexes with multiple values
Paginating with a collection index is straightforward. Collection
indexes have no values
specified, so the index stores a Ref for each
document from the index’s source collection.
How would you handle pagination for an index with values
specified?
Let’s find out! Here, we create a new index for the People
collection
that specifies multiple fields in values
. Copy the following query,
paste it into the Shell, and run it:
CreateIndex({
name: "all_people_all_fields",
source: Collection("People"),
values: [
{ field: ["data", "first"] },
{ field: ["data", "last"] },
{ field: ["data", "letter"] },
{ field: ["ref"] }
]
})
This index records all of the fields from our People
documents as well
as each document’s Ref.
Now, let’s see the results. Copy the following query, paste it into the Shell, and run it:
Paginate(Match(Index("all_people_all_fields")))
When you run the query, the result should be similar to:
{
"data": [
[
"Alan",
"Perlis",
"A",
Ref(Collection("People"), "240166254282805769")
],
[
"Alan",
"Turing",
"B",
Ref(Collection("People"), "240166254282801673")
],
[
"Grace",
"Hopper",
"C",
Ref(Collection("People"), "240166254282806793")
],
[
"Leslie",
"Lamport",
null,
Ref(Collection("People"), "240166254282803721")
],
[
"Marvin",
"Minsky",
1,
Ref(Collection("People"), "240166254282804745")
],
[
"Stephen",
"Cook",
"F",
Ref(Collection("People"), "240166254282807817")
],
[
"Tim",
"Cook",
"G",
Ref(Collection("People"), "240166254282802697")
]
]
}
That worked well. Simple. Easy. So, what’s the problem? Let’s try operating on each document, like we’ve done previously. Copy the following query, paste is into the Shell, and run it:
Map(
Paginate(Match(Index("all_people_all_fields"))),
Lambda("X", Get(Var("X")))
)
This query is also very similar to previous queries. So, it should work fine. When you run the query, the result should be similar to:
invalid argument
Identifier expected, Array provided.
position: ["map","expr","get"]
What went wrong? The "shape" of our index is different. Since the index
contains all of the fields in each document, as well as the document’s
Ref, the Lambda
function needs to accept exactly the same variables as
defined in the index’s values
.
The fix is easy. Copy the following query, paste it into the shell, and run it:
Map(
Paginate(Match(Index("all_people_all_fields"))),
Lambda(
["X", "Y", "Z", "ref"],
Get(Var("ref"))
)
)
This query changes the Lambda
function to accept the variables X
,
Y
, Z
, and ref
. This matches the number of fields in the index.
We’re not going to use the values from the document’s fields, so they
can have generic variable names. But we are going to use the document’s
Ref, so we give that variable the name ref
so that we know what it
contains.
When you run the query, the result should be similar to:
{
"data": [
{
"ref": Ref(Collection("People"), "240166254282805769"),
"ts": 1565299238420000,
"data": {
"first": "Alan",
"last": "Perlis",
"letter": "A"
}
},
{
"ref": Ref(Collection("People"), "240166254282801673"),
"ts": 1565299238420000,
"data": {
"first": "Alan",
"last": "Turing",
"letter": "B"
}
},
{
"ref": Ref(Collection("People"), "240166254282806793"),
"ts": 1565299238420000,
"data": {
"first": "Grace",
"last": "Hopper",
"letter": "C"
}
},
{
"ref": Ref(Collection("People"), "240166254282803721"),
"ts": 1565299238420000,
"data": {
"first": "Leslie",
"last": "Lamport"
}
},
{
"ref": Ref(Collection("People"), "240166254282804745"),
"ts": 1565299238420000,
"data": {
"first": "Marvin",
"last": "Minsky",
"letter": 1
}
},
{
"ref": Ref(Collection("People"), "240166254282807817"),
"ts": 1565299238420000,
"data": {
"first": "Stephen",
"last": "Cook",
"letter": "F"
}
},
{
"ref": Ref(Collection("People"), "240166254282802697"),
"ts": 1565299238420000,
"data": {
"first": "Tim",
"last": "Cook",
"letter": "G"
}
}
]
}
Always remember: Lambda
functions must accept the same number of
variables, and in the same order, as the "tuples" (or items) in the set
being processed (our index represents the set of documents in the source
collection).
Conclusion
This tutorial has demonstrated several different scenarios for
paginating query results, how to access following pages, and how to deal
with indexes with multiple fields defined in values
. These strategies
should help you query your documents in collections large and small!
Was this article helpful?
We're sorry to hear that.
Tell us how we can improve!
documentation@fauna.com
Thank you for your feedback!