How to Browse and Query Firestore Data

Updated

Browsing Firestore looks simple until a collection grows past a few thousand documents. Then pagination, sorting, counts, and indexes all start to matter. This guide explains how Firestore queries actually work, so you can browse large collections quickly and without surprise read costs, whether in code or in a tool.

Listing collections

Firestore has no schema, so collections exist only while they contain documents. Root collections are listed with listCollections() in the Admin SDK (or listCollectionIds in the REST API). Subcollections are listed the same way from a document reference. Deleting a document does not delete its subcollections, so “orphaned” subcollections can remain under documents that no longer exist.

Paginate with cursors, not offsets

offset(n) still reads and bills the skipped documents. For browsing, use a cursor: order the query, remember the last document of the page, and start the next page after it.

const page = await db.collection("orders")
  .orderBy("createdAt", "desc")
  .limit(25)
  .get();

const last = page.docs[page.docs.length - 1];
const next = await db.collection("orders")
  .orderBy("createdAt", "desc")
  .startAfter(last)
  .limit(25)
  .get();

Each page costs one read per document returned, no matter how deep you go. To go back, keep the start cursor of each page you've visited.

Sorting hides documents missing the field

When you order by a field, Firestore only returns documents that have that field. If a collection has older documents without createdAt, sorting by it silently drops them. Ordering by document ID (__name__) is the only ordering that includes every document.

Filters and the index rules

  • Single-field equality and range filters work out of the box, because Firestore indexes every field automatically.
  • If you filter with an inequality (<, >, !=) on one field, that field must be the first ordering. Sorting by a different field after it needs a composite index.
  • Combining an equality filter on one field with ordering on another also needs a composite index.
  • When an index is missing, the error message contains a link that creates it. Index builds usually take a few minutes.

Types matter when you compare

Firestore compares values by type first. The number 42 and the string "42" never match each other, and integers and doubles compare numerically. If a filter unexpectedly returns nothing, check the stored type.

“Starts with” search

Firestore has no full-text search, but you can do a prefix match on a string field with a range:

db.collection("users")
  .where("name", ">=", "Ann")
  .where("name", "<", "Ann\uf8ff")

This is case-sensitive. For real search, sync data to a search service.

Counting documents

Don't read a whole collection to count it. Use the count() aggregation, which is billed at one read per batch of up to 1,000 index entries:

const snapshot = await db.collection("orders")
  .where("status", "==", "paid")
  .count()
  .get();
console.log(snapshot.data().count);

Browsing without writing code

Firewatch applies all of the above for you: it lists collections per database, pages with cursors, shows accurate totals from count(), lets you filter with ==, !=, ranges, starts-with and array-contains, sorts by clicking a column, and links to the missing index when a query needs one. See the Firestore admin GUI guide for what else to look for in a tool.

    How to Browse and Query Firestore Data | Firewatch.io