Firestore's where clause filters documents using operators like ==, !=, <, >, in, not-in, array-contains, and array-contains-any. Chain multiple where calls for compound queries, but follow Firestore's rules: range filters on different fields require a composite index, and you can only use one array-contains or one in operator per query. Use the modular v9+ query and where imports for tree-shaking.
Filtering Firestore Documents with the Where Clause
The where clause is the primary way to filter documents in Firestore queries. This tutorial covers every operator, shows how to chain multiple conditions for compound queries, explains when composite indexes are needed, and demonstrates common patterns like range queries, membership checks, and array filtering. All examples use the modular v9+ SDK syntax.
Prerequisites
- A Firebase project with Firestore database created
- Firebase JS SDK v9+ installed
- At least one Firestore collection with documents to query
- Basic understanding of Firestore documents and collections
Step-by-step guide
Use basic equality and comparison operators
Use basic equality and comparison operators
The most common operators are == for exact match and <, >, <=, >= for range comparisons. Import query, where, collection, and getDocs from firebase/firestore. Build the query by chaining where clauses inside the query function.
1import { getFirestore, collection, query, where, getDocs } from 'firebase/firestore'23const db = getFirestore()45// Exact match6const activeUsersQuery = query(7 collection(db, 'users'),8 where('status', '==', 'active')9)1011// Range comparison12const expensiveProductsQuery = query(13 collection(db, 'products'),14 where('price', '>', 100)15)1617const snapshot = await getDocs(activeUsersQuery)18snapshot.forEach((doc) => {19 console.log(doc.id, doc.data())20})Expected result: Only documents matching the where condition are returned.
Filter with in and not-in operators
Filter with in and not-in operators
The in operator matches documents where a field equals any value in a provided array (up to 30 values). The not-in operator matches documents where the field does not equal any value in the array (up to 10 values). These are useful for matching multiple possible values without writing separate queries.
1// Match any of these categories2const categoriesQuery = query(3 collection(db, 'products'),4 where('category', 'in', ['electronics', 'clothing', 'books'])5)67// Exclude specific statuses8const nonDraftQuery = query(9 collection(db, 'posts'),10 where('status', 'not-in', ['draft', 'archived'])11)Expected result: Documents matching any of the specified values are returned for in; documents not matching any value are returned for not-in.
Query arrays with array-contains and array-contains-any
Query arrays with array-contains and array-contains-any
Use array-contains to find documents where an array field includes a specific value. Use array-contains-any to match documents where the array contains at least one value from a provided list. You can only use one array-contains or array-contains-any per query.
1// Find posts tagged with 'firebase'2const firebasePostsQuery = query(3 collection(db, 'posts'),4 where('tags', 'array-contains', 'firebase')5)67// Find posts tagged with any of these8const multiTagQuery = query(9 collection(db, 'posts'),10 where('tags', 'array-contains-any', ['firebase', 'react', 'typescript'])11)Expected result: Documents containing the specified values in their array fields are returned.
Build compound queries with multiple where clauses
Build compound queries with multiple where clauses
Chain multiple where calls to apply AND logic. Firestore evaluates all conditions and returns only documents matching every clause. Equality filters on different fields work without a composite index. Range filters on multiple different fields or combining range with inequality on another field require a composite index.
1// Compound equality — no composite index needed2const adminActiveQuery = query(3 collection(db, 'users'),4 where('role', '==', 'admin'),5 where('status', '==', 'active')6)78// Equality + range — requires composite index9const recentElectronicsQuery = query(10 collection(db, 'products'),11 where('category', '==', 'electronics'),12 where('price', '<', 500),13 where('price', '>', 50)14)Expected result: Documents matching all where conditions are returned. If an index is needed, Firestore provides a creation link in the error message.
Combine where with orderBy and limit
Combine where with orderBy and limit
Add orderBy to sort filtered results and limit to cap the number of documents returned. When using a range filter (>, <, >=, <=), the first orderBy must be on the same field as the range filter. This is a Firestore requirement for index efficiency.
1import { query, where, orderBy, limit, collection } from 'firebase/firestore'23const topElectronicsQuery = query(4 collection(db, 'products'),5 where('category', '==', 'electronics'),6 orderBy('price', 'asc'),7 limit(10)8)910// Range filter requires orderBy on the same field first11const recentExpensiveQuery = query(12 collection(db, 'products'),13 where('price', '>', 100),14 orderBy('price', 'desc'),15 orderBy('createdAt', 'desc'),16 limit(20)17)Expected result: Filtered documents are returned sorted by the specified field, limited to the requested count.
Use where with real-time listeners
Use where with real-time listeners
Queries with where clauses work with onSnapshot for real-time updates. Firestore only sends changes that match your filter, so listeners are efficient even on large collections. The listener fires with the initial matching documents and then with updates as documents enter or leave the filter.
1import { query, where, collection, onSnapshot } from 'firebase/firestore'23const activeOrdersQuery = query(4 collection(db, 'orders'),5 where('status', '==', 'pending')6)78const unsubscribe = onSnapshot(activeOrdersQuery, (snapshot) => {9 snapshot.docChanges().forEach((change) => {10 if (change.type === 'added') {11 console.log('New pending order:', change.doc.data())12 }13 if (change.type === 'removed') {14 console.log('Order no longer pending:', change.doc.id)15 }16 })17})1819// Call unsubscribe() when done to stop listeningExpected result: The listener fires when a document matches or no longer matches the where condition, enabling real-time filtered views.
Complete working example
1// src/lib/firestore-queries.ts2import {3 getFirestore,4 collection,5 query,6 where,7 orderBy,8 limit,9 getDocs,10 onSnapshot,11 type QueryConstraint12} from 'firebase/firestore'1314const db = getFirestore()1516// Reusable query builder17export async function queryCollection(18 collectionName: string,19 constraints: QueryConstraint[]20) {21 const q = query(collection(db, collectionName), ...constraints)22 const snapshot = await getDocs(q)23 return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))24}2526// Example: Get active users with a specific role27export async function getActiveAdmins() {28 return queryCollection('users', [29 where('role', '==', 'admin'),30 where('status', '==', 'active'),31 orderBy('createdAt', 'desc'),32 limit(50)33 ])34}3536// Example: Get products in a price range37export async function getProductsByPriceRange(38 minPrice: number,39 maxPrice: number,40 category?: string41) {42 const constraints: QueryConstraint[] = [43 where('price', '>=', minPrice),44 where('price', '<=', maxPrice),45 orderBy('price', 'asc'),46 limit(25)47 ]48 if (category) {49 constraints.unshift(where('category', '==', category))50 }51 return queryCollection('products', constraints)52}5354// Example: Real-time listener for filtered data55export function onPendingOrders(56 callback: (orders: any[]) => void57) {58 const q = query(59 collection(db, 'orders'),60 where('status', '==', 'pending'),61 orderBy('createdAt', 'desc')62 )63 return onSnapshot(q, (snapshot) => {64 const orders = snapshot.docs.map((doc) => ({65 id: doc.id,66 ...doc.data()67 }))68 callback(orders)69 })70}Common mistakes when using Where Clause in Firestore
Why it's a problem: Combining range filters on different fields without creating a composite index, resulting in the error 'The query requires an index'
How to avoid: Click the link in the error message to create the required composite index in the Firebase Console, or add it to firestore.indexes.json and deploy with firebase deploy --only firestore:indexes.
Why it's a problem: Using both array-contains and in in the same query, which Firestore does not allow
How to avoid: Split the query into two separate queries and merge results client-side. Alternatively, restructure your data model to use maps instead of arrays for multi-value filtering.
Why it's a problem: Adding orderBy on a field different from the range filter field as the first sort, causing a Firestore error
How to avoid: When using a range filter (>, <, >=, <=), the first orderBy must be on the same field. Add additional orderBy clauses after.
Why it's a problem: Expecting where to match documents where the field does not exist — Firestore only returns documents where the filtered field is present
How to avoid: Firestore queries only match documents that contain the field being filtered. If you need to find documents missing a field, query all documents and filter client-side, or set a default value on every document.
Best practices
- Use equality filters before range filters in your query chain for readability and to match Firestore's index requirements
- Create composite indexes proactively by defining them in firestore.indexes.json rather than waiting for error messages
- Use limit() on every query to control costs — without it, a query can return an entire collection worth of reads
- Prefer array-contains for tag/category filtering and in for matching a set of specific IDs
- Use onSnapshot instead of getDocs when you need the UI to update as the underlying data changes
- Keep the in operator array under 30 elements; split larger sets into batched queries
- For complex querying needs beyond what Firestore supports natively, consider integrating Algolia or RapidDev to build a custom query layer
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Show me all Firestore where clause operators with examples: ==, !=, <, >, <=, >=, in, not-in, array-contains, and array-contains-any. Include a compound query with multiple where clauses, the orderBy and limit combination, and explain when a composite index is needed.
Create a Firestore query helper that supports filtering products by category (equality), price range (min/max), and tags (array-contains). Use Firebase modular SDK v9+ with TypeScript. Include both one-time getDocs and real-time onSnapshot versions. Show the composite index definition needed.
Frequently asked questions
Can I use OR logic in Firestore where clauses?
Firestore added the or() function in SDK v9.18+. You can write or(where('status', '==', 'active'), where('status', '==', 'pending')). For older SDKs, run separate queries and merge results client-side.
How many where clauses can I chain in one query?
There is no hard limit on the number of equality where clauses. However, you can only have range filters on a single field per query (unless using composite indexes), one array-contains, and one in or not-in per query.
Why does my where query return empty results even though documents exist?
Check three things: (1) the field name in your where clause matches the exact field name in your documents, (2) the data type matches (string vs number), and (3) if you have security rules, they may be blocking the query even if the documents match.
Do where queries charge per document scanned or per document returned?
Firestore charges per document returned by the query, not per document scanned. However, a query that returns zero results still incurs a minimum of one read charge.
Can I filter by a field in a nested object?
Yes. Use dot notation in the field name: where('address.city', '==', 'New York'). This works for top-level maps. For arrays of objects, you cannot filter by fields inside array elements — restructure your data or use a subcollection.
What is the maximum number of values in an in clause?
The in and array-contains-any operators accept up to 30 values (increased from 10 in SDK updates). The not-in operator accepts up to 10 values.
Do I need a composite index for every compound query?
Not always. Queries with only equality filters on different fields use single-field indexes (created automatically). You need a composite index when combining equality with range on different fields, or when combining where with orderBy on a different field.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation