Skip to content

Firestore Queries Guide: Filter, Sort, Index & Paginate Data

DodaTech Updated Jun 6, 2026 7 min read

Firestore queries filter, sort, and paginate data using the document/collection model with where clauses, range filters, and composite indexes.

What You’ll Learn

  • Basic queries with where() filters and comparison operators
  • Sorting with orderBy() and limiting results
  • Compound queries and composite indexes
  • Pagination with cursors and limit()
  • Collection group queries for cross-collection search
  • Real-time query listeners

Why Queries Matter

Storing data is useless if you can’t retrieve it efficiently. Firestore’s query engine lets you filter millions of documents by field values, sort by timestamps, and paginate results — all with millisecond latency. DodaTech’s Durga Antivirus Pro queries Firestore to find all devices with critical threats, sort by severity, and paginate results for the dashboard — returning only the most urgent threats without scanning every document.

    flowchart LR
    A["Firestore\nCollection"] -->|"where('severity','==','critical')"| B["Filtered Results"]
    A -->|"orderBy('detectedAt','desc')"| C["Sorted Results"]
    A -->|"limit(20) + startAfter(cursor)"| D["Paginated Results"]
    B --> E["Queried Results"]
    C --> E
    D --> E
    style A fill:#dbeafe,stroke:#2563eb
    style E fill:#fef3c7,stroke:#d97706
  
Prerequisites: Understanding of Firebase Database (collections, documents). Familiarity with JavaScript promises.

Basic Queries

Simple Where Filter

import { collection, query, where, getDocs } from 'firebase/firestore';

// Find all critical threats
const q = query(collection(db, "threats"),
  where("severity", "==", "critical")
);

const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
  console.log(doc.id, "=>", doc.data().name);
});

Comparison Operators

Firestore supports <, <=, ==, >=, >, !=, array-contains, in, and not-in:

// Threats detected in the last 24 hours
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);

const q = query(collection(db, "threats"),
  where("detectedAt", ">=", yesterday)
);

// Threats with specific names
const q = query(collection(db, "threats"),
  where("name", "in", ["Emotet", "LockBit", "BlackCat"])
);

Sorting with orderBy

import { query, orderBy, limit } from 'firebase/firestore';

// Most recent threats first, limit to 10
const q = query(collection(db, "threats"),
  orderBy("detectedAt", "desc"),
  limit(10)
);

Important rule: If you use a where() filter with a range comparison (<, <=, >, >=, !=), the first orderBy() must be on the same field as the range filter.

// ✅ Valid: orderBy matches the range field
const q = query(collection(db, "threats"),
  where("detectedAt", ">=", yesterday),
  orderBy("detectedAt", "desc")
);

// ❌ Invalid: range on 'detectedAt' but first orderBy is 'name'
const q = query(collection(db, "threats"),
  where("detectedAt", ">=", yesterday),
  orderBy("name", "asc")
);

Compound Queries and Composite Indexes

When you filter and sort on different fields, Firestore needs a composite index:

// This query needs a composite index on [severity, detectedAt]
const q = query(collection(db, "threats"),
  where("severity", "==", "critical"),
  orderBy("detectedAt", "desc"),
  limit(20)
);

What happens without the index? Firestore returns an error with a direct link to create the index in the console. Click the link, confirm, and the index is ready in minutes.

// Creating the index via Firebase CLI
// firebase firestore:indexes

// Output in firestore.indexes.json:
{
  "indexes": [
    {
      "collectionGroup": "threats",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "severity", "order": "ASCENDING" },
        { "fieldPath": "detectedAt", "order": "DESCENDING" }
      ]
    }
  ]
}

Pagination

Offset Pagination (Simple)

const q = query(collection(db, "threats"),
  orderBy("detectedAt", "desc"),
  limit(20)
);
const snapshot = await getDocs(q);

// Page 2 — get next 20
const nextQ = query(collection(db, "threats"),
  orderBy("detectedAt", "desc"),
  limit(20),
  startAfter(snapshot.docs[snapshot.docs.length - 1])
);

Cursor Pagination

// Start from a specific document
const q = query(collection(db, "threats"),
  orderBy("detectedAt", "desc"),
  limit(20),
  startAfter(doc(db, "threats", "threat-050"))
);

// Previous page
const prevQ = query(collection(db, "threats"),
  orderBy("detectedAt", "desc"),
  limit(20),
  endBefore(doc(db, "threats", "threat-050"))
);

Why not use offset? Firestore doesn’t support offset() on large datasets — it would bill for reads of skipped documents. Cursor-based pagination using startAfter and endBefore is efficient and recommended.

Collection Group Queries

Query across all subcollections with the same name, not just under one parent:

import { collectionGroup, query, where } from 'firebase/firestore';

// Find all threats across all devices (not just one device's subcollection)
const q = query(collectionGroup(db, "threats"),
  where("severity", "==", "critical")
);

const snapshot = await getDocs(q);
snapshot.forEach((doc) => {
  console.log(`Threat ${doc.data().name} found in ${doc.ref.path}`);
});
// Output: Threat Emotet found in devices/device-abc/threats/threat-001
//         Threat LockBit found in devices/device-def/threats/threat-002

Common Mistakes

1. Querying Without Filters

Reading an entire collection (getDocs(collection(db, "devices"))) on a production app reads every document — expensive and slow. Always use where() and limit().

2. Missing Composite Index Errors

Adding orderBy to a filtered query often requires a composite index. The error message includes a clickable link to create it — don’t ignore it.

3. Using != on Large Collections

The != (not-equal) query requires a composite index and can be slow on large collections. If possible, rephrase as a positive match (e.g., in with specific values instead of !=).

4. Forgetting Query Speed Is Tied to Indexes

Every query in Firestore uses an index. If your query pattern isn’t covered by an existing index, create one. Well-indexed queries return in milliseconds; unindexed queries fail.

5. Not Using limit() in List Views

Without limit(), queries may return thousands of documents, causing slow renders and high Firestore bills. Always paginate with limit().

Practice Questions

  1. What is a composite index and when do you need one?
  2. Why doesn’t Firestore support OFFSET for pagination?
  3. What is a collection group query and when would you use it?
  4. What happens if you use where() with a range filter but orderBy on a different field?
  5. How do you paginate forward using cursor-based pagination?

Answers:

  1. A composite index indexes multiple fields together. You need one when you filter on one field and sort on a different field.
  2. Offset would require reading and billing for all skipped documents. Cursor-based pagination is more efficient because it uses the index directly.
  3. A collection group query searches across all subcollections with the same name, regardless of parent document. Useful for finding all threats across all devices.
  4. The query fails with an error requiring a composite index that includes both fields.
  5. Use startAfter(lastDoc) where lastDoc is the last document from the previous page, combined with orderBy and limit.

Challenge: Write a Firestore query for Durga Antivirus Pro that finds the 10 most recent high-severity threats across all devices, sorted by detection time descending. Then write the paginated version for page 2. Include the composite index configuration.

FAQ

How many indexes can I create in Firestore?
: Firestore supports up to 200 composite indexes per database. Automatic single-field indexes are unlimited. For large applications, plan indexes carefully to stay within limits.
Can I query across multiple collections at once?
: Not in a single query — each query targets one collection or one collection group. To combine results from different collections, run separate queries and merge on the client.
What is the difference between startAt and startAfter?
: startAt includes the specified document in results; startAfter excludes it. Use startAfter(lastDoc) for pagination to avoid showing the same document again.
How do I handle full-text search in Firestore?
: Firestore doesn’t support full-text search. Use a third-party search service like Algolia or MeiliSearch, and sync Firestore data to it via Cloud Functions.

Try It Yourself

// Query all critical threats, sorted by detection time
import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore';

async function getCriticalThreats() {
  const q = query(
    collection(db, "threats"),
    where("severity", "==", "critical"),
    orderBy("detectedAt", "desc"),
    limit(10)
  );
  
  const snapshot = await getDocs(q);
  snapshot.forEach(doc => console.log(doc.data().name, doc.data().detectedAt));
}

What’s Next

TopicDescription
Security Rules & HostingProtect your data and deploy app
Firestore & Realtime DBReview data modeling fundamentals
Firebase Auth GuideAuthenticate users for query access
MongoDB QueriesCompare NoSQL query patterns across databases

What’s Next

Congratulations on completing this Firebase Queries tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro