144

You can get the child count via

firebase_node.once('value', function(snapshot) { alert('Count: ' + snapshot.numChildren()); });

But I believe this fetches the entire sub-tree of that node from the server. For huge lists, that seems RAM and latency intensive. Is there a way of getting the count (and/or a list of child names) without fetching the whole thing?

TomStr
  • 51
  • 5
josh
  • 9,038
  • 8
  • 31
  • 37

4 Answers4

108

The code snippet you gave does indeed load the entire set of data and then counts it client-side, which can be very slow for large amounts of data.

Firebase doesn't currently have a way to count children without loading data, but we do plan to add it.

For now, one solution would be to maintain a counter of the number of children and update it every time you add a new child. You could use a transaction to count items, like in this code tracking upvodes:

var upvotesRef = new Firebase('https://docs-examples.firebaseio.com/android/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes');
upvotesRef.transaction(function (current_value) {
  return (current_value || 0) + 1;
});

For more info, see https://www.firebase.com/docs/transactions.html

UPDATE: Firebase recently released Cloud Functions. With Cloud Functions, you don't need to create your own Server. You can simply write JavaScript functions and upload it to Firebase. Firebase will be responsible for triggering functions whenever an event occurs.

If you want to count upvotes for example, you should create a structure similar to this one:

{
  "posts" : {
    "-JRHTHaIs-jNPLXOQivY" : {
      "upvotes_count":5,
      "upvotes" : {
      "userX" : true,
      "userY" : true,
      "userZ" : true,
      ...
    }
    }
  }
}

And then write a javascript function to increase the upvotes_count when there is a new write to the upvotes node.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.countlikes = functions.database.ref('/posts/$postid/upvotes').onWrite(event => {
  return event.data.ref.parent.child('upvotes_count').set(event.data.numChildren());
});

You can read the Documentation to know how to Get Started with Cloud Functions.

Also, another example of counting posts is here: https://github.com/firebase/functions-samples/blob/master/child-count/functions/index.js

Update January 2018

The firebase docs have changed so instead of event we now have change and context.

The given example throws an error complaining that event.data is undefined. This pattern seems to work better:

exports.countPrescriptions = functions.database.ref(`/prescriptions`).onWrite((change, context) => {
    const data = change.after.val();
    const count = Object.keys(data).length;
    return change.after.ref.child('_count').set(count);
});

```

Mike
  • 81
  • 1
  • 4
Andrew Lee
  • 10,127
  • 3
  • 46
  • 40
  • 79
    Did you ever add support for this? – Jim Cooper Feb 23 '14 at 01:49
  • 2
    We haven't added it yet. – Andrew Lee Feb 25 '14 at 01:01
  • 21
    Is a client-side counter in a transaction secure though? It seems it could be easily hacked to artificially increase counts. This could be bad for voting systems. – Soviut Apr 07 '14 at 06:10
  • 17
    ++ it would be really nice to get the count without incurring transfer costs – Jared Forsyth Aug 09 '14 at 21:41
  • @Soviut, since the transaction runs client-side for each user, any artificial inflation would only affect the user who did the hacking. it wouldn't become system-wide. – Dustin Nov 08 '14 at 03:33
  • 28
    Has this ever been added? – Eliezer Jun 05 '15 at 03:57
  • 2
    @Dustin obviously the value is being updated in the database and it's going to affect every users who come afterwards, because the count needs to be world-writable. Imagine implementing a voting system or something that limits the quota of users with this code--the result could be disastrous. – Prathan Thananart Jun 14 '15 at 14:53
  • 26
    Any news on this feature roadmap ? Thanks – Pandaiolo Jul 27 '15 at 15:30
  • 4
    Any news? This feature would be really useful. – Blodhgard Feb 19 '16 at 08:23
  • 4
    You plan to add it when? – cncool Mar 06 '16 at 02:22
  • 3
    Their REST api has parameter called shallow=true, that fetches only immediate children and not subchildren and it works well if you use REST api through say curl. But js api doesn't have anything similar – A Paracha May 20 '16 at 02:10
  • 1
    Shallow is probably the best option for now, but it's not returned with compression and can become pretty slow and experience for large data sets – Mbrevda Jun 24 '16 at 09:35
  • 3
    This is a must have feature and answer here is useless as it does download the whole node. – Gayan Pathirage Nov 29 '16 at 09:27
  • 1
    any news on this ? please – daslicht Dec 15 '16 at 16:05
  • we need an easy way to get the count. maintaining a counter in all nodes is not an easy task. – alltej Feb 27 '17 at 23:08
  • 2
    We have cloud functions right now and official guide provides child nodes count example. I'd recommend checking it because it covers some edge cases. https://github.com/firebase/functions-samples/tree/master/child-count – Michał Pietraszko Mar 27 '17 at 10:52
  • 1
    The javascript syntax is incorrect (old?). I used this worked: `exports.countposts = functions.database.ref('/bangumis/{bangumiId}/posts').onWrite(event => { return event.data.ref.parent.child('posts_count').set(event.data.numChildren()); });` – Jonny Jun 21 '17 at 08:34
  • 2
    I also note that if you/people adds lots of data at a time, the data writes (or similar) do not necessarily happen in the correct order, so you might get lower values reappearing suddenly. I display this value on the client and I kind of fix it by only showing a new value to the user if the value is higher than the previous one. – Jonny Jun 21 '17 at 09:45
  • Update again: I was able to do the "dont write new value if not bigger than existing value" by modifying the javascript a bit more. It does a `on("value" ...` call to fetch the existing value first and when that value comes back (it's not instant), I compare it to the new value and only write the new value if the old one is smaller. It seems to work good so far. – Jonny Jun 21 '17 at 11:54
  • 1
    Me again! This solution died as soon as the data in the collection grew too big, in my case several thousands (about 10000 using test data). There might be a max data limit or similar for when onWrite is used. – Jonny Apr 13 '18 at 13:07
  • In your sample, is there a way to get the 'upvotes_count' without incurring the cost of loading all the upvotes? Wouldn't it be better to have the data separated out? like the post has the normal post data and `upvotes` and `downvotes` fields, but the actual votes could be stored in `upvotes/postid` and `downvotes/postid`? When doing an upvote the function could remove any downvotes and update the totals in the post given the new counts? Those could be write-only and you couildn't tell who has upvoted or downvoted you. – Jason Goemaat Jul 17 '18 at 22:12
  • 2
    THEY WILL NEVER ADD THIS... firebase makes money forcing you to download big packages (even when not necessary)... this is why firebase uses plain http (instead of gzip or something), makes no sence to their business model create a simple way to make you save money – Rafael Lima Mar 01 '19 at 00:48
  • 3
    It's been 7 years, has FireBase added a Realtime Database counter? – DavidTaubmann Feb 15 '20 at 15:02
  • Pardon my beginner question, because I've tried to look for this before without answers.... but: Do cloud functions require an "Admin App" constantly running on some device connected to the same cloud as the other clients to execute the functions, AND is it ONLY available for Cloud Firestore? -- OR -- Is this Admin App runned by Google themselves, AND is it also available for RealTime? – Delark Mar 15 '21 at 14:21
  • Dang, I've really been waiting for this feature...... when can we expect to see counts for nodes out of the box? seems like a very reasonable feature – hugger May 04 '23 at 13:13
41

This is a little late in the game as several others have already answered nicely, but I'll share how I might implement it.

This hinges on the fact that the Firebase REST API offers a shallow=true parameter.

Assume you have a post object and each one can have a number of comments:

{
 "posts": {
  "$postKey": {
   "comments": {
     ...  
   }
  }
 }
}

You obviously don't want to fetch all of the comments, just the number of comments.

Assuming you have the key for a post, you can send a GET request to https://yourapp.firebaseio.com/posts/[the post key]/comments?shallow=true.

This will return an object of key-value pairs, where each key is the key of a comment and its value is true:

{
 "comment1key": true,
 "comment2key": true,
 ...,
 "comment9999key": true
}

The size of this response is much smaller than requesting the equivalent data, and now you can calculate the number of keys in the response to find your value (e.g. commentCount = Object.keys(result).length).

This may not completely solve your problem, as you are still calculating the number of keys returned, and you can't necessarily subscribe to the value as it changes, but it does greatly reduce the size of the returned data without requiring any changes to your schema.

emjames
  • 51
  • 2
  • 6
Alex Klibisz
  • 1,313
  • 1
  • 14
  • 21
  • 1
    Might make this the accepted answer since shallow=true is a new addition since the previous answers. Haven't had time to look into it myself, so will wait a few days to see what people think... – josh Jan 26 '16 at 19:08
  • 1
    Shallow is probably the best option for now, but it's not returned with compression and can become pretty slow and experience for large data sets – Mbrevda Jun 24 '16 at 09:36
  • If the comment keys has no boolean values but instead has children, does it still return the key-value pairs of keys? – alltej Feb 27 '17 at 12:18
  • 1
    You might want to be careful using the REST API: https://startupsventurecapital.com/firebase-costs-increased-by-7-000-81dc0a27271d – Remi Sture Aug 08 '17 at 07:28
  • 5
    Just to point out that you have to append `.json` to the end of the URL, for example: `https://yourapp.firebaseio.com/posts/comments.json?shallow=true` – Osama Xäwãñz Aug 12 '17 at 15:37
  • @OsamaXäwãñz Just FYI, looks like something weird happened with your paste. The `shallow=true` is actually `shallow=t%E2%80%8C%E2%80%8Brue`. I guess it added an invisible character right between T and R. – MortenMoulder Nov 06 '17 at 03:18
  • What is considered large data sets? 10k and above? @Mbrevda – FabricioG Jan 21 '20 at 07:15
  • @FabricioG I haven't used Firebase in over three years now, but the client libraries used to emit warnings if you were running slow queries/operations. Maybe that is still the case. Otherwise "large" is subjective to your use case. For example, if you run this repeatedly in a client-side app, then transferring 10K records will create a poor experience. If it runs once a day on a server then 10K is probably fine. In my case I was using it to count messages in group chats with ~10 participants, so even a few hundred messages was a poor experience. – Alex Klibisz Jan 22 '20 at 22:55
23

Save the count as you go - and use validation to enforce it. I hacked this together - for keeping a count of unique votes and counts which keeps coming up!. But this time I have tested my suggestion! (notwithstanding cut/paste errors!).

The 'trick' here is to use the node priority to as the vote count...

The data is:

vote/$issueBeingVotedOn/user/$uniqueIdOfVoter = thisVotesCount, priority=thisVotesCount vote/$issueBeingVotedOn/count = 'user/'+$idOfLastVoter, priority=CountofLastVote

,"vote": {
  ".read" : true
  ,".write" : true
  ,"$issue" : {
    "user" : {
      "$user" : {
        ".validate" : "!data.exists() && 
             newData.val()==data.parent().parent().child('count').getPriority()+1 &&
             newData.val()==newData.GetPriority()" 

user can only vote once && count must be one higher than current count && data value must be same as priority.

      }
    }
    ,"count" : {
      ".validate" : "data.parent().child(newData.val()).val()==newData.getPriority() &&
             newData.getPriority()==data.getPriority()+1 "
    }

count (last voter really) - vote must exist and its count equal newcount, && newcount (priority) can only go up by one.

  }
}

Test script to add 10 votes by different users (for this example, id's faked, should user auth.uid in production). Count down by (i--) 10 to see validation fail.

<script src='https://cdn.firebase.com/v0/firebase.js'></script>
<script>
  window.fb = new Firebase('https:...vote/iss1/');
  window.fb.child('count').once('value', function (dss) {
    votes = dss.getPriority();
    for (var i=1;i<10;i++) vote(dss,i+votes);
  } );

function vote(dss,count)
{
  var user='user/zz' + count; // replace with auth.id or whatever
  window.fb.child(user).setWithPriority(count,count);
  window.fb.child('count').setWithPriority(user,count);
}
</script>

The 'risk' here is that a vote is cast, but the count not updated (haking or script failure). This is why the votes have a unique 'priority' - the script should really start by ensuring that there is no vote with priority higher than the current count, if there is it should complete that transaction before doing its own - get your clients to clean up for you :)

The count needs to be initialised with a priority before you start - forge doesn't let you do this, so a stub script is needed (before the validation is active!).

pperrin
  • 1,487
  • 15
  • 33
  • This is awesome!!! What happens on conflicts, though? Ie, two people vote on the same time? Ideally you'd want to automatically resolve that, instead of just discarding one of their votes... maybe do the vote in a transaction? – josh Apr 17 '14 at 21:06
  • Hi Josh, logically a genuine vote can only fail if a previous vote has been cast, but the total not updated (yet). My 2nd to last para covers that - I'd just do the total update for previous voters vote anyway (everytime) - if it wasn't needed, so what? and then this votes updates. As long as the vote works fine. If your 'total' update fails, next voter will fix it, so again - so what? – pperrin Apr 17 '14 at 23:30
  • I am really tempted to just say the 'count' node should be a 'last previous vote' node - so each voter/client updates/fixes/repairs that node/value and then adds its own vote - (letting the next voter update the total to include 'this' vote). -- If you get my drift... – pperrin Apr 17 '14 at 23:39
4

write a cloud function to and update the node count.

// below function to get the given node count.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.userscount = functions.database.ref('/users/')
    .onWrite(event => {

      console.log('users number : ', event.data.numChildren());


      return event.data.ref.parent.child('count/users').set(event.data.numChildren());
    }); 

Refer :https://firebase.google.com/docs/functions/database-events

root--| |-users ( this node contains all users list) |
|-count |-userscount : (this node added dynamically by cloud function with the user count)

indvin
  • 41
  • 1
  • 3