Firestore Queries Explained: Filters, Ordering, Indexes, and Pagination

This page explains how Firestore queries work in Google Cloud: which operators are available, when composite indexes are required, how cursor pagination works, and how to avoid the mistakes that cause queries to fail or cost more than expected. Query design in Firestore is closely tied to index management and data modelling, so understanding it early saves a lot of debugging later.

What Firestore queries actually do

In a relational database, you can filter rows by any column in any combination and the database works out how to execute the query. Firestore works differently.

Every Firestore query runs against a pre-built index. When you query for orders where status == “shipped”, Firestore looks up matching entries in an index for the status field and fetches those documents directly. It does not scan the entire collection. This is why Firestore stays fast regardless of how large the collection grows.

The trade-off is that not all queries are supported by default. If the index for a particular field combination does not exist, the query fails. Single-field indexes are created automatically for every field in every document. Multi-field queries require composite indexes you create and manage yourself.

This also means your Firestore data model must be designed around the queries you intend to run. You cannot design the document structure first and write queries after the fact, as you would in a relational database.

Analogy

Think of the index at the back of a textbook. If the term you need is listed there, you can jump straight to the right pages. If it is not listed, you have to read the whole book. Firestore works the same way: indexed field combinations let it jump straight to matching documents. Combinations that lack an index result in a query error, not a slow scan.

How Firestore queries work

Firestore supports two types of queries.

A collection query targets a single collection at a specific path. A query on users searches only the top-level users collection. A query on users/alice/orders searches only Alice’s orders subcollection.

A collection group query targets all collections with the same name, regardless of where they sit in the document hierarchy. A collection group query on orders searches across users/alice/orders, users/bob/orders, and every other subcollection named orders in the database.

Both query types support filters, sorting, limits, and cursor pagination. The difference is scope.

Queries fail for several reasons beyond syntax errors:

  • A required composite index has not been created yet
  • An inequality filter is on one field but the first orderBy is on a different field
  • A field has been excluded from automatic indexing but the query tries to filter by it
  • A collection group query is missing its required composite index
Reads are billed per document, not per query

If you fetch an entire collection and filter the results in application code, you pay for every document read, including the ones you discard. Reading 10,000 documents to show 20 results costs 10,000 reads. Always push filter logic into Firestore rather than doing it in your backend. The Firestore overview covers the full billing model.

Queries run from client SDKs such as mobile or web apps are also subject to Firestore Security Rules. A query that your server-side code can execute may be rejected when run from a client if the rules do not permit it.

Basic query examples

These examples use the Node.js SDK and cover patterns common to real applications. The gcloud CLI can also list collection documents for quick inspection.

# List all documents in a collection via gcloud
gcloud firestore documents list \
  --collection-ids=users \
  --database='(default)'
// Get all documents in a collection
const usersRef = db.collection('users');
const snapshot = await usersRef.get();

// Get active users (single-field equality filter, no extra config needed)
const activeUsers = await db.collection('users')
  .where('status', '==', 'active')
  .get();

// Get orders created after a specific date (range filter on a Timestamp field)
const recentOrders = await db.collection('orders')
  .where('createdAt', '>=', new Date('2026-01-01'))
  .get();

// Get products within a price range (two conditions on the same field)
const affordableProducts = await db.collection('products')
  .where('price', '>=', 10)
  .where('price', '<=', 100)
  .get();

// Get the latest approved comments (filter + orderBy on different fields; composite index required)
const approvedComments = await db.collection('comments')
  .where('approved', '==', true)
  .orderBy('createdAt', 'desc')
  .limit(20)
  .get();

The first four examples use single-field indexes that Firestore creates automatically. The fifth combines a filter on one field with ordering on a different field, which requires a composite index covering both fields.

Filtering operators

Firestore supports the following operators inside where() clauses:

  • == equal to a specific value
  • != not equal to a specific value
  • <, <=, >, >= comparison operators for ranges
  • in field value matches one of a list (up to 30 values)
  • not-in field value does not match any value in a list (up to 10 values)
  • array-contains array field contains a specific value
  • array-contains-any array field contains at least one value from a list

A few constraints are worth knowing before you write your first multi-field query. You can use at most one array-contains or array-contains-any per query. You cannot combine in and array-contains-any in the same query. Inequality filters on multiple different fields in the same query require composite indexes and carry strict ordering rules.

Firestore does not support full-text search

Equality and range operators work on exact values only. Firestore cannot search within strings, match partial words, or do case-insensitive lookups. If your application needs search-as-you-type or keyword matching, integrate a dedicated service such as Algolia or Elasticsearch alongside Firestore. Trying to replicate search with filters does not work well in practice.

Ordering results

Use orderBy() to sort query results. You can sort ascending or descending, and chain multiple orderBy() calls to sort on secondary fields.

// Sort products by price ascending
const cheapFirst = await db.collection('products')
  .orderBy('price', 'asc')
  .get();

// Get shipped orders sorted by date, newest first (composite index required)
const shippedOrders = await db.collection('orders')
  .where('status', '==', 'shipped')
  .orderBy('createdAt', 'desc')
  .get();
Inequality filters constrain your ordering

If a query uses an inequality operator (!=, <, <=, >, >=), the first orderBy() call must be on the same field as the inequality filter. Ordering by createdAt when the inequality is on price causes a query error. This catches a lot of developers by surprise the first time they combine range filters with sorting.

Add a secondary sort for reliable pagination

Firestore does not guarantee a stable sort order when multiple documents share the same value in the sort field. For reliable pagination, add a secondary orderBy() on a unique field such as the document ID. This makes your page cursors deterministic.

Pagination with cursors

Firestore uses cursor-based pagination. Instead of skipping a number of rows as SQL’s OFFSET does, you pass a reference to the last document on the current page. The next page query starts after that document. Firestore does not need to count or skip anything, so this approach scales well regardless of how far into a collection you paginate.

Analogy

A cursor works like a bookmark in a book. When you come back to continue reading, you open to the bookmarked page and carry on from there. You do not count pages from the front each time. Firestore cursors work the same way: you hand Firestore a document reference, and it resumes the query immediately after that point in the index.

// First page: get the first 20 products by price
const firstPage = await db.collection('products')
  .orderBy('price', 'asc')
  .limit(20)
  .get();

// Capture the last document as the cursor for the next page
const lastVisible = firstPage.docs[firstPage.docs.length - 1];

// Next page: start after the cursor document
const secondPage = await db.collection('products')
  .orderBy('price', 'asc')
  .startAfter(lastVisible)
  .limit(20)
  .get();

// Go back: end before the first document on the current page
const firstVisible = secondPage.docs[0];
const prevPage = await db.collection('products')
  .orderBy('price', 'asc')
  .endBefore(firstVisible)
  .limitToLast(20)
  .get();

This pattern works well for “load more” buttons and infinite scroll interfaces. It is awkward for numbered page navigation (“jump to page 5”) because you need to have previously loaded the last document on page 4 to use as the cursor. If your UI requires numbered pages, consider storing the cursor document reference for each page as the user navigates forward.

Single-field and composite indexes

Firestore automatically creates a single-field index for every field in every document. Any query that filters or sorts on a single field works immediately without configuration. You can exempt specific fields from automatic indexing to reduce index storage costs, but only for fields you are certain will never appear in a query filter or sort clause.

# Exempt a high-cardinality field from automatic indexing
gcloud firestore fields update lastUpdated \
  --collection-group=logs \
  --index-config='{"indexes": []}'

When a query filters or sorts on more than one field, Firestore needs a composite index. If the required index does not exist, the query fails with an error that includes a console link to create it.

Use the link in the error message

When a query fails because of a missing composite index, Firestore includes a direct console URL in the error message. Clicking it opens the index creation page with the fields and sort directions already filled in. This is the fastest way to create the correct index without manually specifying each field.

# Create a composite index via gcloud
gcloud firestore indexes composite create \
  --collection-group=orders \
  --field-config="field-path=status,order=ASCENDING" \
  --field-config="field-path=createdAt,order=DESCENDING"

# List all composite indexes
gcloud firestore indexes composite list
Deploy indexes before the code that needs them

Composite indexes take several minutes to build after creation. If you ship application code that runs a query against an unfinished index, every request touching that query fails until the index is ready. In production deployments, always confirm the index status in the Firestore console or via gcloud firestore indexes composite list before releasing the dependent code.

{
  "indexes": [
    {
      "collectionGroup": "orders",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "status", "order": "ASCENDING" },
        { "fieldPath": "createdAt", "order": "DESCENDING" }
      ]
    }
  ]
}

Collection group queries

A collection group query searches all collections with a given name, regardless of their position in the document hierarchy. This is useful when data is stored in subcollections under many different parent documents and you need to search across all of them at once.

For example, if each blog post has a comments subcollection, a collection group query on comments returns results from all posts in a single query, without needing to know any individual post’s document path.

// Query all "comments" subcollections across all blog posts
const recentApproved = await db.collectionGroup('comments')
  .where('approved', '==', true)
  .orderBy('createdAt', 'desc')
  .limit(50)
  .get();

Collection group queries always require a composite index that includes the name field. Firestore returns an error with an index creation link if the required index does not exist. The Firestore Data Model guide covers how subcollections fit into the broader document hierarchy and when to use them instead of top-level collections.

When Firestore queries fit

Firestore queries are a good fit for applications organised around well-defined access patterns. Common examples include:

  • User profiles and settings documents
  • Content feeds ordered by date or score
  • Order histories filtered by status
  • Real-time collaborative features where clients subscribe to a live query
  • Mobile and web app backends with predictable, repeated read patterns

Firestore queries run into friction with:

  • Complex relational queries that require joining data across collections
  • Heavy ad hoc filtering across many unpredictable field combinations
  • Full-text search requirements
  • Analytical reporting and aggregations across large datasets
  • Extremely high write throughput on a single document or hot collection (consider Bigtable for write-heavy time-series workloads)

If you are still deciding which database fits your requirements, the Choosing the Right Storage Service guide walks through the decision for all major GCP storage options.

Firestore vs Cloud SQL: query model differences

Developers moving from relational databases to Firestore are often surprised by the query constraints. The root difference is the query model.

Cloud SQL lets you write arbitrary SQL against any combination of columns. You can add indexes later to improve performance, but queries work even without them. Firestore is the opposite: a query without a supporting index fails with an error, not just slower results. The index is a prerequisite, not a performance optimisation.

Cloud SQL is also the better choice when you need joins across multiple tables, aggregate functions, or flexible conditions you cannot predict in advance. Firestore is designed for document lookups by a defined set of access patterns, not open-ended querying.

For a detailed comparison across scalability, transactions, pricing, and use cases, see the Cloud SQL vs Firestore comparison.

Common beginner mistakes

  1. Designing the data model before understanding query patterns. Firestore queries cannot be written freely after the fact. If your document structure does not align with the queries you need, you end up filtering in application code or restructuring the database. Define the queries your application needs first, then design documents and indexes to serve them.

  2. Deploying code before the composite index it needs is ready. New composite indexes take several minutes to build. If your application runs a query before its index has finished, every request fails until the index is complete. Deploy firestore.indexes.json and confirm the index status in the console before rolling out the code that depends on it.

  3. Filtering in application code instead of in Firestore. Fetching all documents and filtering the results in your backend costs one read per document fetched, regardless of how many you actually use. A query that fetches 10,000 documents to return 200 costs 10,000 reads. Push filtering logic into Firestore wherever possible.

  4. Expecting Firestore to do full-text search. Equality and range filters work on exact values. Firestore cannot search within strings, match partial words, or rank results by relevance. If your application requires search-as-you-type or keyword matching, you need a dedicated search service running alongside Firestore.

  5. Getting the inequality ordering rule wrong. When a query includes an inequality filter, the first orderBy() call must be on the same field as the inequality filter. Ordering by createdAt when the inequality is on price causes a query error. Align the first sort field with the inequality field, or restructure the query.

  6. Applying SQL offset thinking to Firestore pagination. Firestore does not have a OFFSET equivalent. Attempting to replicate it by fetching extra documents and discarding them costs one read for every discarded document. Use startAfter() with a document cursor instead.

Frequently asked questions

Why does my Firestore query fail even though the code looks right?

The most common cause is a missing composite index. Firestore runs every query against an index. Single-field queries work automatically because Firestore creates single-field indexes for each field by default. Multi-field queries (filtering by one field and ordering by another, or filtering on multiple fields at once) require a composite index you create manually. The error message Firestore returns includes a direct link to create the missing index in the console. Click it, wait for the index to build, then retry the query.

When do I need a composite index in Firestore?

You need a composite index any time a query filters or sorts on more than one field simultaneously. Filtering by status and ordering by createdAt is a common example that requires a composite index covering both fields in the correct order. Collection group queries also always require composite indexes. As a practical rule: when Firestore returns a query error, the error message contains a direct link to create the required index in the console.

Does Firestore support full-text search?

No. Firestore does not support full-text search natively. The array-contains operator can check whether an exact value exists in an array field, but it does not search within strings or support partial matching. For full-text search, use a dedicated service such as Algolia, Elasticsearch, or Typesense alongside Firestore. If your application needs search-as-you-type or substring matching, Firestore is not the right choice for that requirement.

What is the difference between a collection query and a collection group query?

A collection query targets one specific collection at a known path, such as all documents in users/alice/orders. A collection group query targets all collections with the same name anywhere in the database. A collection group query on "orders" returns documents from users/alice/orders, users/bob/orders, and every other orders subcollection in one result set. Collection group queries require a composite index and are useful when you need to search across subcollections without knowing every parent document path in advance.

Is Firestore good for complex reporting queries?

Not really. Firestore is designed for serving documents and lists of documents by known access patterns. It works well for user profiles, content feeds, and real-time app views. It is not designed for SQL-style joins, multi-field aggregations, flexible ad hoc filtering, or analytical reporting. For reporting workloads, the recommended approach is to export Firestore data to BigQuery and run queries there.

Last verified: 23 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.