Firestore is powerful, but inefficient queries can increase costs, slow performance, and hit usage limits. Here’s how to optimize queries for large datasets effectively.
Firestore doesn’t support joins like SQL, so data modeling is crucial.
Instead of storing all messages inside a chat document (bad for scalability):
* Use subcollections for messages:
/chats/{chatId}/messages/{messageId}
Firestore automatically indexes fields used in simple queries, but complex queries (e.g., multiple where
conditions or orderBy
) require composite indexes.
db.collection("orders")
.where("status", "==", "shipped")
.where("customerId", "==", "user123")
.orderBy("orderDate", "desc")
.limit(10);
* Firestore will reject this query unless you create a composite index.
status
(Filter)customerId
(Filter)orderDate
(Sort: Descending).select()
to Reduce Data TransferBy default, Firestore returns all fields in a document, even if you don’t need them.
db.collection("users")
.select("name", "email") // Fetch only name & email (not full profile)
.get();
* Reduces data transfer cost and query execution time.
.startAfter()
Fetching too much data at once can cause slow performance and expensive reads.
let firstQuery = db.collection("posts")
.orderBy("createdAt", "desc")
.limit(20);
let snapshot = await firstQuery.get();
let lastVisible = snapshot.docs[snapshot.docs.length - 1]; // Get last doc
let nextQuery = db.collection("posts")
.orderBy("createdAt", "desc")
.startAfter(lastVisible) // Fetch next page
.limit(20);
Firestore processes where()
before orderBy()
.
Mistake : Ordering first and then filtering results in errors.
db.collection("products")
.where("category", "==", "electronics") // Filter first
.orderBy("price", "asc") // Then sort
.get();
!=
& Array Queries When PossibleFirestore does not support !=
queries directly, and array queries can be costly.
db.collection("users")
.where("role", "!=", "admin") // Firestore does not support '!='
.get();
array-contains-any
db.collection("users")
.where("role", "in", ["editor", "viewer"]) // Instead of '!='
.get();
in
(max 10 values) but not !=
.If old data isn’t needed, automate deletion to reduce query size.
timestamp
in documents.exports.cleanupOldLogs = functions.pubsub.schedule('every 24 hours').onRun(async () => {
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days ago
const query = db.collection("logs").where("createdAt", "<", cutoff);
const snapshot = await query.get();
let batch = db.batch();
snapshot.forEach(doc => batch.delete(doc.ref));
await batch.commit();
});
* Keeps dataset small, improving query performance.
.onSnapshot()
Firestore re-fetches all matching documents when using real-time updates.
docChanges()
to Track Only New Datadb.collection("messages")
.orderBy("timestamp", "desc")
.limit(20)
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type === "added") {
console.log("New message:", change.doc.data());
}
});
});
Issue | Solution |
---|---|
Large data sets | Use subcollections instead of nested arrays |
Slow queries | Index fields used in where() & orderBy() |
High read costs | Use .select() to fetch only necessary fields |
Too much data at once | Use pagination (startAfter() ) |
Sorting before filtering | Always filter first, then sort |
Inefficient real-time updates | Use docChanges() instead of full reloads |
Too many old documents | Set up TTL & batch deletes |