Write-time aggregations

Queries in Firestore let you find documents in large collections. To gain insight into properties of the collection as a whole, you can aggregate data over a collection.

You can aggregate data either at read-time or at write time:

  • Read-time aggregations calculate a result at the time of the request. Firestore supports the count(), sum(), and average() aggregation queries at read-time. Read-time aggregation queries are are easier to add to your app than write-time aggregations. For more on aggregation queries, see Summarize data with aggregation queries.

  • Write-time aggregations calculate a result each time the app performs a relevant write operation. Write-time aggregations are more work to implement, but you might use them instead of read-time aggregations for one of the following reasons:

    • You want to listen to the aggregation result for real-time updates. The count(), sum(), and average() aggregation queries do not support real-time updates.
    • You want to store the aggregation result in a client-side cache. The count(), sum(), and average() aggregation queries do not support caching.
    • You are aggregating data from tens of thousands of documents for each of your users and consider costs. At a lower number of documents, read-time aggregations cost less. For a large number of documents in an aggregations, write-time aggregations might cost less.

You can implement a write-time aggregation using either a client-side transaction or with Cloud Run functions. The following sections describe how to implement write-time aggregations.

Solution: Write-time aggregation with a client-side transaction

Consider a local recommendations app that helps users find great restaurants. The following query retrieves all the ratings for a given restaurant:

Web

db.collection("restaurants")
  .doc("arinell-pizza")
  .collection("ratings")
  .get();

Swift

Note: This product is not available on watchOS and App Clip targets.
do {
  let snapshot = try await db.collection("restaurants")
    .document("arinell-pizza")
    .collection("ratings")
    .getDocuments()
  print(snapshot)
} catch {
  print(error)
}

Objective-C

Note: This product is not available on watchOS and App Clip targets.
FIRQuery *query = [[[self.db collectionWithPath:@"restaurants"]
    documentWithPath:@"arinell-pizza"] collectionWithPath:@"ratings"];
[query getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot,
                                    NSError * _Nullable error) {
  // ...
}];

Kotlin
Android

db.collection("restaurants")
    .document("arinell-pizza")
    .collection("ratings")
    .get()

Java
Android

db.collection("restaurants")
        .document("arinell-pizza")
        .collection("ratings")
        .get();

Rather than fetching all ratings and then computing aggregate information, we can store this information on the restaurant document itself:

Web

var arinellDoc = {
  name: 'Arinell Pizza',
  avgRating: 4.65,
  numRatings: 683
};

Swift

Note: This product is not available on watchOS and App Clip targets.
struct Restaurant {

  let name: String
  let avgRating: Float
  let numRatings: Int

}

let arinell = Restaurant(name: "Arinell Pizza", avgRating: 4.65, numRatings: 683)

Objective-C

Note: This product is not available on watchOS and App Clip targets.
@interface FIRRestaurant : NSObject

@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) float averageRating;
@property (nonatomic, readonly) NSInteger ratingCount;

- (instancetype)initWithName:(NSString *)name
               averageRating:(float)averageRating
                 ratingCount:(NSInteger)ratingCount;

@end

@implementation FIRRestaurant

- (instancetype)initWithName:(NSString *)name
               averageRating:(float)averageRating
                 ratingCount:(NSInteger)ratingCount {
  self = [super init];
  if (self != nil) {
    _name = name;
    _averageRating = averageRating;
    _ratingCount = ratingCount;
  }
  return self;
}

@end.m