Skip to main content
FirestoreORM provides a powerful, type-safe query builder for complex data retrieval. This guide covers filtering, sorting, aggregations, and advanced query patterns.

Query Builder Basics

Starting a Query

All queries start with the query() method on your repository.
const query = userRepo.query();
The query builder is chainable, allowing you to add multiple conditions:
const activeUsers = await userRepo.query()
  .where('status', '==', 'active')
  .where('age', '>', 18)
  .orderBy('createdAt', 'desc')
  .limit(10)
  .get();

Filtering with Where Clauses

Equality Operators

Check if a field equals or doesn’t equal a value.
// Exact match
const activeUsers = await userRepo.query()
  .where('status', '==', 'active')
  .get();

// Not equal
const nonAdmins = await userRepo.query()
  .where('role', '!=', 'admin')
  .get();

Comparison Operators

Compare numeric, date, and string values.
// Greater than
const adults = await userRepo.query()
  .where('age', '>', 18)
  .get();

// Greater than or equal
const seniors = await userRepo.query()
  .where('age', '>=', 65)
  .get();

// Less than
const recentOrders = await orderRepo.query()
  .where('createdAt', '<', yesterday)
  .get();

// Less than or equal
const affordableProducts = await productRepo.query()
  .where('price', '<=', 100)
  .get();

In and Not-In Queries

Match against multiple possible values.
// In operator (max 10 values)
const pendingOrProcessing = await orderRepo.query()
  .where('status', 'in', ['pending', 'processing', 'shipped'])
  .get();

// Not-in operator (max 10 values)
const nonCancelledOrders = await orderRepo.query()
  .where('status', 'not-in', ['cancelled', 'refunded'])
  .get();
Firestore limits in and not-in queries to 10 values maximum.

Array Operations

Query documents based on array field contents.
// Array contains a specific value
const jsDevs = await userRepo.query()
  .where('skills', 'array-contains', 'javascript')
  .get();

// Array contains any of the specified values
const webDevs = await userRepo.query()
  .where('skills', 'array-contains-any', ['javascript', 'typescript', 'react'])
  .get();

Multiple Where Clauses

Chain multiple conditions to narrow your results.
const results = await orderRepo.query()
  .where('status', '==', 'pending')
  .where('total', '>', 100)
  .where('createdAt', '>=', startOfMonth)
  .get();
Firestore requires composite indexes for certain query combinations. The ORM will provide a link to create the index if needed.

Sorting Results

Single Field Ordering

// Ascending order (default)
const usersByName = await userRepo.query()
  .orderBy('name', 'asc')
  .get();

// Descending order
const recentUsers = await userRepo.query()
  .orderBy('createdAt', 'desc')
  .get();

Multi-Field Ordering

Sort by multiple fields in sequence.
const products = await productRepo.query()
  .orderBy('category', 'asc')
  .orderBy('price', 'desc')
  .get();
// Results: sorted by category first, then by price within each category
When combining where() and orderBy(), your first orderBy() field must match your inequality filter field or you’ll need a composite index.

Limiting Results

Basic Limit

Restrict the number of documents returned.
// Get top 5 products by price
const topProducts = await productRepo.query()
  .orderBy('price', 'desc')
  .limit(5)
  .get();

// Get first 20 users
const users = await userRepo.query()
  .limit(20)
  .get();

Pagination

See the Pagination Guide for detailed pagination strategies.

Counting Documents

Count Query Results

Get the number of documents matching your query without fetching them.
// Count active users
const activeCount = await userRepo.query()
  .where('status', '==', 'active')
  .count();

console.log(`${activeCount} active users`);

Total Collection Count

Get the total number of documents in the collection.
// Total count (excludes soft-deleted by default)
const total = await userRepo.query().totalCount();

// Total including deleted
const totalWithDeleted = await userRepo.query()
  .includeDeleted()
  .totalCount();
count() is more efficient than fetching all documents and checking the array length.

Existence Checks

Check if any documents match your query.
// Check if email is already taken
const emailExists = await userRepo.query()
  .where('email', '==', newEmail)
  .exists();

if (emailExists) {
  throw new Error('Email already in use');
}

// Check if user has any orders
const hasOrders = await orderRepo.query()
  .where('userId', '==', userId)
  .exists();

Getting Single Results

Get One Document

Retrieve the first document matching your query.
// Find user by email
const user = await userRepo.query()
  .where('email', '==', '[email protected]')
  .getOne();

if (user) {
  console.log('Found:', user.name);
} else {
  console.log('User not found');
}

// Get the cheapest product
const cheapest = await productRepo.query()
  .where('category', '==', 'books')
  .orderBy('price', 'asc')
  .getOne();

Field Selection

Retrieve only specific fields to reduce bandwidth.
// Get only names and emails
const users = await userRepo.query()
  .where('status', '==', 'active')
  .select('name', 'email')
  .get();

// users = [{ id, name, email }, ...]
// Other fields like age, address are not included
Field selection reduces network transfer but still charges for full document reads in Firestore.

Aggregations

Sum Aggregation

Calculate the total of a numeric field.
// Calculate total revenue
const totalRevenue = await orderRepo.query()
  .where('status', '==', 'completed')
  .aggregate('total', 'sum');

console.log(`Total revenue: $${totalRevenue}`);

Average Aggregation

Calculate the average value of a numeric field.
// Calculate average product rating
const avgRating = await reviewRepo.query()
  .where('productId', '==', productId)
  .aggregate('rating', 'avg');

console.log(`Average rating: ${avgRating.toFixed(2)} stars`);

// Calculate average order value
const avgOrderValue = await orderRepo.query()
  .where('status', '==', 'completed')
  .aggregate('total', 'avg');

Distinct Values

Get all unique values for a specific field.
// Get all product categories
const categories = await productRepo.query()
  .distinctValues('category');
console.log(categories); // ['electronics', 'books', 'clothing']

// Get all order statuses in use
const statuses = await orderRepo.query()
  .where('createdAt', '>', lastMonth)
  .distinctValues('status');

Soft Delete Handling

Default Behavior

By default, all queries exclude soft-deleted documents.
// Only returns non-deleted users
const users = await userRepo.query()
  .where('status', '==', 'active')
  .get();

Include Deleted Documents

// Include soft-deleted documents in results
const allUsers = await userRepo.query()
  .includeDeleted()
  .get();

// Count including deleted
const totalCount = await userRepo.query()
  .includeDeleted()
  .count();

Only Deleted Documents

// Get only soft-deleted users
const deletedUsers = await userRepo.query()
  .onlyDeleted()
  .get();

// Count deleted users from last month
const deletedCount = await userRepo.query()
  .onlyDeleted()
  .where('deletedAt', '>', lastMonth)
  .count();

Query Operations

Update All Matching Documents

Update multiple documents that match your query.
// Mark all pending orders as processing
const updatedCount = await orderRepo.query()
  .where('status', '==', 'pending')
  .update({ status: 'processing' });

console.log(`Updated ${updatedCount} orders`);
See Bulk Operations Guide for more details.

Delete All Matching Documents

// Delete all cancelled orders older than 30 days
const deletedCount = await orderRepo.query()
  .where('status', '==', 'cancelled')
  .where('createdAt', '<', thirtyDaysAgo)
  .delete();

Soft Delete Matching Documents

// Soft delete inactive users
const deletedCount = await userRepo.query()
  .where('lastLogin', '<', oneYearAgo)
  .softDelete();

Complex Query Examples

const results = await productRepo.query()
  .where('category', '==', 'electronics')
  .where('price', '<=', 500)
  .where('inStock', '==', true)
  .orderBy('price', 'asc')
  .limit(20)
  .get();

User Analytics

// Get premium users who signed up this month
const premiumUsers = await userRepo.query()
  .where('plan', '==', 'premium')
  .where('createdAt', '>=', startOfMonth)
  .where('createdAt', '<=', endOfMonth)
  .orderBy('createdAt', 'desc')
  .get();

// Count active users by region
const usRegions = ['us-east', 'us-west', 'us-central'];
const usActiveCount = await userRepo.query()
  .where('status', '==', 'active')
  .where('region', 'in', usRegions)
  .count();

Order Reports

// Get order statistics for a date range
const orders = await orderRepo.query()
  .where('status', '==', 'completed')
  .where('createdAt', '>=', startDate)
  .where('createdAt', '<=', endDate)
  .get();

const totalRevenue = await orderRepo.query()
  .where('status', '==', 'completed')
  .where('createdAt', '>=', startDate)
  .where('createdAt', '<=', endDate)
  .aggregate('total', 'sum');

const avgOrderValue = await orderRepo.query()
  .where('status', '==', 'completed')
  .where('createdAt', '>=', startDate)
  .where('createdAt', '<=', endDate)
  .aggregate('total', 'avg');

console.log({
  totalOrders: orders.length,
  totalRevenue,
  avgOrderValue
});

Composite Index Errors

Firestore requires composite indexes for certain query combinations.
try {
  const results = await orderRepo.query()
    .where('status', '==', 'pending')
    .where('total', '>', 100)
    .orderBy('createdAt', 'desc')
    .get();
} catch (error) {
  if (error instanceof FirestoreIndexError) {
    console.log(error.toString());
    // Logs: "Firestore index required. Create it here: https://..."
    // Click the link, wait 1-2 minutes, then retry
  }
}
The error message includes a direct link to create the required index in the Firebase Console.

Performance Tips

1

Always Use Limits

Prevent accidentally fetching thousands of documents.
// ✅ Good - controlled result size
await userRepo.query().limit(100).get();

// ❌ Bad - could fetch entire collection
await userRepo.query().get();
2

Use count() Instead of Fetching

When you only need the count, don’t fetch the documents.
// ✅ Efficient
const total = await userRepo.query()
  .where('status', '==', 'active')
  .count();

// ❌ Expensive
const users = await userRepo.query()
  .where('status', '==', 'active')
  .get();
const total = users.length;
3

Use exists() for Presence Checks

Check existence without fetching documents.
// ✅ Reads at most 1 document
const hasOrders = await orderRepo.query()
  .where('userId', '==', userId)
  .exists();

// ❌ Reads all matching documents
const orders = await orderRepo.query()
  .where('userId', '==', userId)
  .get();
const hasOrders = orders.length > 0;
4

Order Before Filtering When Possible

Firestore can use indexes more efficiently with this pattern.
await productRepo.query()
  .orderBy('price', 'desc')
  .where('price', '>', 100)
  .limit(20)
  .get();

Next Steps

Pagination

Learn cursor-based and offset pagination strategies

Streaming

Process large datasets efficiently with streaming

Real-time

Subscribe to live query updates with onSnapshot

Bulk Operations

Efficiently update or delete multiple documents

Build docs developers (and LLMs) love