28

I'm looking for a way to sort a nested array of objects.

Here's an example:

{
  answers : [
    { name : 'paul',  state : 'RU' },
    { name : 'steve', state : 'US' }, 
    { name : 'mike',  state : 'DE' }, 
    ...
  ]
}

Suppose now I want to find all the name, of the answers array, but how can I sort them in ascending order?

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
Kaylit
  • 281
  • 1
  • 3
  • 4

6 Answers6

36

I would store it in the order you want it back out. Or sort it after you pull it out, on the client side.

If neither of those are possible, you can use the aggregation framework:

> db.test.insert({answers: [
...                 {name: 'paul', state: 'RU'},
...                 {name: 'steve', state: 'US'}, 
...                 {name: 'mike', state: 'DE'}]});
> db.test.insert({answers: [
...                 {name: 'paul', state: 'RU'},
...                 {name: 'steve', state: 'US'}, 
...                 {name: 'xavier', state: 'TX'}]});

db.test.aggregate([
  {$unwind: "$answers"}, 
  {$sort: {"answers.name":1}}, 
  {$group: {_id:"$_id", answers: {$push:"$answers"}}}
]);

produces:

{
  "result" : [
  {
    "_id" : ObjectId("5053b2477d820880c3469364"),
    "answers" : [
      {
        "name" : "paul",
        "state" : "RU"
      },
      {
        "name" : "steve",
        "state" : "US"
      },
      {
        "name" : "xavier",
        "state" : "TX"
      }
    ]
  },
  {
    "_id" : ObjectId("5053af9f7d820880c3469363"),
    "answers" : [
      {
        "name" : "mike",
        "state" : "DE"
      },
      {
        "name" : "paul",
        "state" : "RU"
      },
      {
        "name" : "steve",
        "state" : "US"
      }
    ]
  }
],
  "ok" : 1
}
B T
  • 57,525
  • 34
  • 189
  • 207
Eve Freeman
  • 32,467
  • 4
  • 86
  • 101
5

This might not be helpful in this context, but I thought I'd add this. You also have the option to sort it on the write which tends to be better for denormalized collections where you don't have to sort in more than one way.

I found this situation in my app when creating a feed on a user.

Meteor.users.helpers({
  'addToFeed': function (gameId, pushData) {
    check(pushData, FeedSchema);
    Meteor.users.update({
      _id: this._id,
      "games.gameId": gameId
    }, {
      $push: {
        "games.$.feed": {
          $each: [pushData],
          $sort: { timestamp: -1 }
        }
      }
    });
  }
});

I found it to be pretty handy because you can then use find() and it will be sorted by your specifications by default.

corvid
  • 10,733
  • 11
  • 61
  • 130
  • I'm getting a minimongo error about not being able to set field named $... Any ideas? I think this is really close to what I want – lol Nov 17 '15 at 01:29
  • Does anyone know if this i a better/faster approach than keeping the array unsorted and sort in the find query? – Cache Aug 07 '23 at 13:02
4

Starting in Mongo 4.4, the $function aggregation operator allows applying a custom javascript function to implement behaviour not supported by the MongoDB Query Language.

For instance, in order to sort an array of objects by one of their fields:

// {
//   "answers" : [
//     { "name" : "steve",  "state" : "US" },
//     { "name" : "xavier", "state" : "FR" },
//     { "name" : "paul",   "state" : "RU" }
//   ]
// }
db.collection.aggregate(
  { $set:
    { "answers":
      { $function: {
          body: function(answers) { return answers.sort((a, b) => a.name > b.name); },
          args: ["$answers"],
          lang: "js"
      }}
    }
  }
)
// {
//   "answers" : [
//     { "name" : "paul",   "state" : "RU" },
//     { "name" : "steve",  "state" : "US" },
//     { "name" : "xavier", "state" : "FR" }
//   ]
// }

This modifies the array in place, without having to apply a combination of expensive $unwind, $sort and $group stages.

$function takes 3 parameters:

  • body, which is the function to apply, whose parameter is the array to modify.
  • args, which contains the fields from the record that the body function takes as parameter. In our case "$answers".
  • lang, which is the language in which the body function is written. Only js is currently available.
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
  • It might be worthwhile to note that the M0/M2/M5 tiers for MongoDB clusters do not support $function operators in aggregation. Source: https://docs.atlas.mongodb.com/reference/unsupported-commands/ – gezquinndesign Dec 03 '20 at 09:58
3

Since mongodb version 5.2, you can use $sortArray:

db.engineers.aggregate([
   {$set:
     {answers: {$sortArray: {input: "$answers ", sortBy: { name: 1 } }}}
   }
])
nimrod serok
  • 14,151
  • 2
  • 11
  • 33
-1

I have found one solution for this while updating the nested array we can also sort that, after updating we will have an sorted array then we don't need to sort on find

db.students.update(
   { _id: 1 },
   {
     $push: {
       quizzes: {
         $each: [ `enter code here`{ id: 3, score: 8 }, { id: 4, score: 7 }, { id: 5, score: 6 } ],
         $sort: { score: 1 }
       }
     }
   }
)
-2

After performing a find() you can use sort() on the return value.

db.collection.find({},{"answers.name":1}).sort({"answers.name":1})

The find would extract the name fields of all documents in the collection. The sort would then sort them by name, ascending.

http://www.mongodb.org/display/DOCS/Sorting+and+Natural+Order

Philipp
  • 67,764
  • 9
  • 118
  • 153
  • 1
    You need quotes around your `"answers.name"` keys. But I don't think your code does what he's looking for... – Eve Freeman Sep 14 '12 at 22:59
  • 6
    Your example will list all documents with their `_id` field and `name` array, sorted by the first name in each array. The `sort()` works at the document level and does not reorder arrays within the documents. – Stennie Sep 15 '12 at 02:29