How can I paginate Firestore results between a lower and upper bound using query cursors without fetching previous pages?
0 reputation · 25 Dec 2020, 14:00 UTC
0 reputation · 25 Dec 2020, 14:00 UTC
I need to retrieve a specific page of results from a Firestore collection ordered by population, while restricting the results to a defined lower and upper population bound (e.g., between 1,000,000 and 5,000,000). The collection is large and frequently updated, so I want to avoid reading all preceding pages to reach the desired page. I plan to use query cursors (startAt/startAfter and endAt/endBefore) combined with limit() to paginate, but I am unsure how to compute the cursors for an arbitrary page without iterating through earlier batches.
How can I derive a start cursor for page N directly using document snapshots that satisfy the bound? Is it safe to reuse the same end cursor for all pages, or must it be recomputed when data changes? What are the consistency guarantees if documents are added or removed between queries?
27025 reputation · 26 Dec 2020, 01:36 UTC
Firestore cursor pagination between numeric bounds works when the bounded field is also the field used in orderBy(). If the population field drives both the range filter and the ordering, you can derive a start cursor for page N by capturing the last document snapshot from the preceding page and applying startAfter() on a reused base query.
db.collection('cities')
.where('population', '>=', 1000000)
.where('population', '<=', 5000000)
.orderBy('population')
.limit(pageSize)
.startAfter(lastDoc):.startAfter(lastDoc).limit(pageSize)
.endBefore(firstDoc).To verify the approach works for your data, confirm that the field used in where() bounds is identical to the field used in orderBy(). If they differ, cursor‑based pagination may produce unexpected results.
Use comments to ask for clarification. Post a solution as an answer.
1,900 reputation · 25 Dec 2020, 20:09 UTC
One clarification worth adding: you cannot derive a cursor for an arbitrary page N purely from the bounds. Cursors are positional — startAfter() needs a document snapshot (or exact field values) from the preceding page, so reaching page N without holding page N-1's last document means either reading through earlier pages or using offset(), which still bills you for the skipped documents. There is no "seek to row 500" in Firestore.
What you can do without reading prior pages is anchor the window itself: orderBy('population').startAt(1000000).endAt(5000000).limit(pageSize) returns the first page inside the bound directly, and the read count equals the window size, not the collection size. Each subsequent page then chains off the previous page's last snapshot.
Two practical follow-ups:
DocumentSnapshot to startAfter() rather than the raw number, or add a unique tiebreaker (e.g., a second orderBy on document ID) — otherwise boundary documents can be skipped or duplicated.Assumes a recent Firestore client SDK (v9+ modular API or equivalent); cursor semantics described are long-standing, but confirm against your SDK version.