59

In Firestore, how can I get the total number of documents in a collection?

For instance if I have

/people
    /123456
        /name - 'John'
    /456789
        /name - 'Jane'

I want to query how many people I have and get 2.

I could do a query on /people and then get the length of the returned results, but that seems a waste, especially because I will be doing this on larger datasets.

Dan McGrath
  • 41,220
  • 11
  • 99
  • 130
justinbc820
  • 963
  • 1
  • 10
  • 15
  • 3
    I've been using db.collection('products').get().then(res => console.log(res.size)) which gives me the number of documents in that collection which seems to work – Ben Cochrane Mar 21 '18 at 12:01
  • 1
    @BenCochrane, that's not going to work if you have a large number of documents. – Nelson La Rocca Sep 26 '20 at 22:40
  • I think you might also be interested in this article, [How to count the number of documents in a Firestore collection?](https://medium.com/firebase-tips-tricks/how-to-count-the-number-of-documents-in-a-firestore-collection-3bd0c719978f). – Alex Mamo Jul 21 '21 at 08:11
  • With the new version of Firebase you can now run aggregated queries! Simply write .count().get(); after your query. – mik Oct 24 '22 at 09:39

10 Answers10

50

You currently have 3 options:

Option 1: Client side

This is basically the approach you mentioned. Select all from collection and count on the client side. This works well enough for small datasets but obviously doesn't work if the dataset is larger.

Option 2: Write-time best-effort

With this approach, you can use Cloud Functions to update a counter for each addition and deletion from the collection.

This works well for any dataset size, as long as additions/deletions only occur at the rate less than or equal to 1 per second. This gives you a single document to read to give you the almost current count immediately.

If need need to exceed 1 per second, you need to implement distributed counters per our documentation.

Option 3: Write-time exact

Rather than using Cloud Functions, in your client you can update the counter at the same time as you add or delete a document. This means the counter will also be current, but you'll need to make sure to include this logic anywhere you add or delete documents.

Like option 2, you'll need to implement distributed counters if you want to exceed per second

Community
  • 1
  • 1
Dan McGrath
  • 41,220
  • 11
  • 99
  • 130
  • 9
    Just wondering, is there likely to be other ways of doing this in the future? Something simple like the SQL COUNT aggregate function that would work on large datasets while maintaining good performance? – saricden Dec 30 '17 at 07:33
  • 3
    @saricden COUNT aggregate actual has a lot of potential performances issues. The Cloud Firestore system is designed for operations that keep the same performance characteristics regardless dataset size, which COUNT doesn't. We are looking into options in the future that strike a balance. – Dan McGrath Feb 22 '18 at 21:26
  • Why didn't I think of option 3!! My app uses 5 shards for every document, multiplying the number of reads by 6 every time I retrieve the document. This was really inefficient and costing too much money. Option 3 allows me to update the document total every time a shard is incremented. It costs 2 writes instead of 1 but it's really worth it on the read side. – Alan Nelson Aug 10 '18 at 13:09
  • 1
    @DanMcGrath I'm new to FireStore, but as I see it, option 3 is problematic for client side security-wide. Say I can like a post. But I must be also able to update the counter. So if I'm a malicious user I can set it to any value. Is there a solution for this? – Paul Dec 08 '18 at 23:20
  • I am doubt about Option 3.Because the document you store count number never know how much documents gonna write to collection.one way is limit to one write per operation.like `request.resource.data.count.size() -1 or +1 == resource.data.count.size()` If you want to write more documents, except you know every single case you gonna write how much documents to collection or there is no way you can write rule for it. – flutroid Aug 18 '19 at 10:48
  • 1
    For option 2, how can we make sure that the same Firestore event delivered twice, does not result in inc/decrementing the counter twice? Is it only possible with a transaction on `eventId`? – dan Oct 27 '19 at 07:47
9

Aggregations are the way to go (firebase functions looks like the recommended way to update these aggregations as client side exposes info to the user you may not want exposed) https://firebase.google.com/docs/firestore/solutions/aggregation

Another way (NOT recommended) which is not good for large lists and involves downloading the whole list: res.size like this example:

   db.collection("logs")
      .get()
      .then((res) => console.log(res.size));
Michael Nelles
  • 5,426
  • 8
  • 41
  • 57
Ben Cochrane
  • 3,317
  • 1
  • 14
  • 16
  • 7
    This requires that you download the entire products collection in order to get the size. That can be extremely detrimental for large collections – justinbc820 Mar 28 '18 at 20:01
  • @justinbc820 you are correct - I have ammended my answer to recommend aggregations instead https://firebase.google.com/docs/firestore/solutions/aggregation – Ben Cochrane Mar 29 '18 at 00:35
  • 1
    This doesn't require you to download the entire collection if you have a where clause that should limit results to between 1 and 0, for example. Like mine – SeanMC Jul 02 '19 at 04:25
  • The res.size is always returning zero Not sure what I am doing wrong. – Venkat Kotra Dec 06 '19 at 02:40
4

If you use AngulareFire2, you can do (assuming private afs: AngularFirestore is injected in your constructor):

this.afs.collection(myCollection).valueChanges().subscribe( values => console.log(values.length));

Here, values is an array of all items in myCollection. You don't need metadata so you can use valueChanges() method directly.

DaveyDaveDave
  • 9,821
  • 11
  • 64
  • 77
Johan Chouquet
  • 427
  • 6
  • 13
2

Be careful counting number of documents for large collections with a cloud function. It is a little bit complex with firestore database if you want to have a precalculated counter for every collection.

Code like this doesn't work in this case:

export const customerCounterListener = 
    functions.firestore.document('customers/{customerId}')
    .onWrite((change, context) => {

    // on create
    if (!change.before.exists && change.after.exists) {
        return firestore
                 .collection('metadatas')
                 .doc('customers')
                 .get()
                 .then(docSnap =>
                     docSnap.ref.set({
                         count: docSnap.data().count + 1
                     }))
    // on delete
    } else if (change.before.exists && !change.after.exists) {
        return firestore
                 .collection('metadatas')
                 .doc('customers')
                 .get()
                 .then(docSnap =>
                     docSnap.ref.set({
                         count: docSnap.data().count - 1
                     }))
    }

    return null;
});

The reason is because every cloud firestore trigger has to be idempotent, as firestore documentation say: https://firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees

Solution

So, in order to prevent multiple executions of your code, you need to manage with events and transactions. This is my particular way to handle large collection counters:

const executeOnce = (change, context, task) => {
    const eventRef = firestore.collection('events').doc(context.eventId);

    return firestore.runTransaction(t =>
        t
         .get(eventRef)
         .then(docSnap => (docSnap.exists ? null : task(t)))
         .then(() => t.set(eventRef, { processed: true }))
    );
};

const documentCounter = collectionName => (change, context) =>
    executeOnce(change, context, t => {
        // on create
        if (!change.before.exists && change.after.exists) {
            return t
                    .get(firestore.collection('metadatas')
                    .doc(collectionName))
                    .then(docSnap =>
                        t.set(docSnap.ref, {
                            count: ((docSnap.data() && docSnap.data().count) || 0) + 1
                        }));
        // on delete
        } else if (change.before.exists && !change.after.exists) {
            return t
                     .get(firestore.collection('metadatas')
                     .doc(collectionName))
                     .then(docSnap =>
                        t.set(docSnap.ref, {
                            count: docSnap.data().count - 1
                        }));
        }

        return null;
    });

Use cases here:

/**
 * Count documents in articles collection.
 */
exports.articlesCounter = functions.firestore
    .document('articles/{id}')
    .onWrite(documentCounter('articles'));

/**
 * Count documents in customers collection.
 */
exports.customersCounter = functions.firestore
    .document('customers/{id}')
    .onWrite(documentCounter('customers'));

As you can see, the key to prevent multiple execution is the property called eventId in the context object. If the function has been handled many times for the same event, the event id will be the same in all cases. Unfortunately, you must have "events" collection in your database.

Ferran Verdés
  • 306
  • 2
  • 5
1

Please check below answer I found on another thread. Your count should be atomic. Its required to use FieldValue.increment() function in such case.

https://stackoverflow.com/a/49407570/3337028

Nikhil
  • 861
  • 11
  • 15
1

firebase-admin offers select(fields) which allows you to only fetch specific fields for documents within your collection. Using select is more performant than fetching all fields. However, it is only available for firebase-admin and firebase-admin is typically only used server side.

select can be used as follows:

select('age', 'name') // fetch the age and name fields
select() // select no fields, which is perfect if you just want a count

select is available for Node.js servers but I am not sure about other languages:

https://googleapis.dev/nodejs/firestore/latest/Query.html#select https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#select

Here's a server side cloud function written in Node.js which uses select to count a filtered collection and to get the IDs of all resulting documents. Its written in TS but easily converted to JS.

import admin from 'firebase-admin'

// https://stackoverflow.com/questions/46554091/cloud-firestore-collection-count

// we need to use admin SDK here as select() is only available for admin
export const videoIds = async (req: any): Promise<any> => {

  const id: string = req.query.id || null
  const group: string = req.query.group || null
  let processed: boolean = null
  if (req.query.processed === 'true') processed = true
  if (req.query.processed === 'false') processed = false

  let q: admin.firestore.Query<admin.firestore.DocumentData> = admin.firestore().collection('videos')
  if (group != null) q = q.where('group', '==', group)
  if (processed != null) q = q.where('flowPlayerProcessed', '==', processed)
  // select restricts returned fields such as ... select('id', 'name')
  const query: admin.firestore.QuerySnapshot<admin.firestore.DocumentData> = await q.orderBy('timeCreated').select().get()

  const ids: string[] = query.docs.map((doc: admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>) => doc.id) // ({ id: doc.id, ...doc.data() })

  return {
    id,
    group,
    processed,
    idx: id == null ? null : ids.indexOf(id),
    count: ids.length,
    ids
  }
}

The cloud function HTTP request completes within 1 second for a collection of 500 docs where each doc contains a lot of data. Not amazingly performant but much better than not using select. Performance could be improved by introducing client side caching (or even server side caching).

The cloud function entry point looks like this:

exports.videoIds = functions.https.onRequest(async (req, res) => {
  const response: any = await videoIds(req)
  res.json(response)
})

The HTTP request URL would be:

https://SERVER/videoIds?group=my-group&processed=true

Firebase functions detail where the server is located on deployment.

danday74
  • 52,471
  • 49
  • 232
  • 283
0

Following Dan Answer: You can have a separated counter in your database and use Cloud Functions to maintain it. (Write-time best-effort)

// Example of performing an increment when item is added
module.exports.incrementIncomesCounter = collectionRef.onCreate(event => {
  const counterRef = event.data.ref.firestore.doc('counters/incomes')

  counterRef.get()
  .then(documentSnapshot => {
    const currentCount = documentSnapshot.exists ? documentSnapshot.data().count : 0

    counterRef.set({
      count: Number(currentCount) + 1
    })
    .then(() => {
      console.log('counter has increased!')
    })
  })
})

This code shows you the complete example of how to do it: https://gist.github.com/saintplay/3f965e0aea933a1129cc2c9a823e74d7

Saint Play
  • 1,083
  • 10
  • 12
  • 3
    Your operation is not atomic. In other words if two increments happen at the same time you might lose one. You can either wrap it in a transaction or use the simpler doc.update("count", firebase.firestore.FieldValue.increment(1)); – Franck Jeannin Aug 03 '19 at 09:08
  • @FranckJeannin Yes, this needs an update, since firebase.firestore.FieldValue.increment is a thing – Saint Play Aug 03 '19 at 22:25
0

I created an NPM package to handle all counters:

First install the module in your functions directory:

npm i adv-firestore-functions

then use it like so:

import { eventExists, colCounter } from 'adv-firestore-functions';

functions.firestore
    .document('posts/{docId}')
    .onWrite(async (change: any, context: any) => {

    // don't run if repeated function
    if (await eventExists(context)) {
      return null;
    }

    await colCounter(change, context);
}

It handles events, and everything else.

If you want to make it a universal counter for all functions:

import { eventExists, colCounter } from 'adv-firestore-functions';

functions.firestore
    .document('{colId}/{docId}')
    .onWrite(async (change: any, context: any) => {

    const colId = context.params.colId;

    // don't run if repeated function
    if (await eventExists(context) || colId.startsWith('_')) {
      return null;
    }

    await colCounter(change, context);
}

And don't forget your rules:

match /_counters/{document} {
  allow read;
  allow write: if false;
}

And of course access it this way:

const collectionPath = 'path/to/collection';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');

Read more: https://code.build/p/9DicAmrnRoK4uk62Hw1bEV/firestore-counters GitHub: https://github.com/jdgamble555/adv-firestore-functions

Jonathan
  • 3,893
  • 5
  • 46
  • 77
0

Get a new write batch

WriteBatch batch = db.batch();

Add a New Value to Collection "NYC"

DocumentReference nycRef = db.collection("cities").document();
batch.set(nycRef, new City());

Maintain a Document with Id as Count and initial Value as total=0

During Add Operation perform like below

DocumentReference countRef= db.collection("cities").document("count");
batch.update(countRef, "total", FieldValue.increment(1));

During Delete Operation perform like below

DocumentReference countRef= db.collection("cities").document("count");
batch.update(countRef, "total", FieldValue.increment(-1));

Always get Document count from

DocumentReference nycRef = db.collection("cities").document("count");
Mithun S
  • 408
  • 8
  • 20
-1

Use Transaction to update the count inside the success listener of your database write.

FirebaseFirestore.getInstance().runTransaction(new Transaction.Function<Long>() {
                @Nullable
                @Override
                public Long apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
                    DocumentSnapshot snapshot = transaction
                            .get(pRefs.postRef(forumHelper.getPost_id()));
                    long newCount;
                    if (b) {
                        newCount = snapshot.getLong(kMap.like_count) + 1;
                    } else {
                        newCount = snapshot.getLong(kMap.like_count) - 1;
                    }

                    transaction.update(pRefs.postRef(forumHelper.getPost_id()),
                            kMap.like_count, newCount);

                    return newCount;
                }
            });
Vikash Sharma
  • 539
  • 8
  • 13