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.
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
orderByis 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
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 rangesinfield value matches one of a list (up to 30 values)not-infield value does not match any value in a list (up to 10 values)array-containsarray field contains a specific valuearray-contains-anyarray 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.
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();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.
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.
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.
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 listComposite 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
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.
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.jsonand confirm the index status in the console before rolling out the code that depends on it.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.
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.
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 bycreatedAtwhen the inequality is onpricecauses a query error. Align the first sort field with the inequality field, or restructure the query.Applying SQL offset thinking to Firestore pagination. Firestore does not have a
OFFSETequivalent. Attempting to replicate it by fetching extra documents and discarding them costs one read for every discarded document. UsestartAfter()with a document cursor instead.
Summary
- Every Firestore query runs against an index. Single-field indexes are automatic; composite indexes must be created manually.
- Multi-field queries (filtering and ordering on different fields) require composite indexes. Missing indexes cause query errors with a console link to fix them.
- Available operators: equality, inequality, comparison,
in,not-in,array-contains,array-contains-any. - Inequality filters require the first
orderBy()to be on the same field as the inequality filter. - Pagination uses document cursors (
startAfter,endBefore), not SQL-style offsets. - Collection group queries search all subcollections with the same name and always require a composite index.
- Firestore is not designed for full-text search, SQL joins, or flexible ad hoc reporting.
- Data modelling and query design must be done together. The shape of your documents must match the queries you need to run.
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.