24

One of my collections is a list of operations (or tasks) with some status. For example, a list of documents can look like this

{
  _id: '57befe57fc956d2e3252890c',
  task: 'Go buy milk',
  status: 'pending'
},
{
  _id: '57befe5efc956d2e3252890d',
  task: 'Cook dinner',
  status: 'complete'
},
{
  _id: '57befe5efc956d2e3252890e',
  task: 'Play games',
  status: 'new'
}

I want to sort this list based on a their status, where new > pending > complete.

How can I do that with MongoDB without having to create an extra field? I am asking as in my case the sorting order may be pre-configured (i.e. users could have their preferences to pending > new > complete for example)

11
  • Is there a reason as to why you cannot handle the sorting on the client side? Best bet would be to return all documents and then use filter to create the subgroups based on the user preference order lodash.com/docs#filter Commented Aug 25, 2016 at 14:46
  • 1
    You could do this with an aggregation pipeline that has two pipelines, $project and $sort. The $project will create an extra field, say score that is populated based on a preconfigured order with the $cond operator, so for instance for the above score for status "new" will be 3, for pending 2 etc, then the $sort pipeline will sort on the score field. Commented Aug 25, 2016 at 14:49
  • 1
    @dyouberg I am using pagination, and the sorting needs to be done before $limit, on the server side, otherwise the pagination is all messed up. Why would you suggest downloading all items to the client? That's a bad use of bandwidth and processing. Commented Aug 25, 2016 at 14:51
  • @chridam so, I cannot just add extra fields, I need to specify them all, adding whatever fields I need as well? Commented Aug 25, 2016 at 14:52
  • 1
    @mido Please look at this solution. $addFields to add new sort field while retaining all the existing fields. This looks exactly what you need. You can add {$project:{"statusValue":0}} to drop the field as the last stage. Commented Dec 27, 2017 at 15:10

2 Answers 2

22

Based on what @chirdam said here is what the implementation looks like. Also the new sorted field is not present in the result as requested.

DataSet :

{
  _id: '57befe57fc956d2e3252890c',
  task: 'Go buy milk',
  status: 'pending'
},
{
  _id: '57befe5efc956d2e3252890d',
  task: 'Cook dinner',
  status: 'complete'
},
{
  _id: '57befe5efc956d2e3252890e',
  task: 'Play games',
  status: 'new'
}

Query :

db.task.aggregate([
    { "$project" : {
        "_id" : 1,
        "task" : 1,
        "status" : 1,
        "order" : {
            "$cond" : {
                if : { "$eq" : ["$status", "new"] }, then : 1,
                else  : { "$cond" : {
                    "if" : { "$eq" : ["$status", "pending"] }, then : 2, 
                    else  : 3
                    }
                }
            }
        }
    } }, 
    {"$sort" : {"order" : 1} },
    { "$project" : { "_id" : 1, "task" : 1, "status" : 1 } }
])

Output:

{ "_id" : "57befe5efc956d2e3252890e", "task" : "Play games", "status" : "new" }
{ "_id" : "57befe57fc956d2e3252890c", "task" : "Go buy milk", "status" : "pending" }
{ "_id" : "57befe5efc956d2e3252890d", "task" : "Cook dinner", "status" : "complete" }
Sign up to request clarification or add additional context in comments.

3 Comments

you can use $addFields instead of project if you do not want to add the existing fields all over again.
Rather than multiple if-then-else sequences you can just put the desired order into an array and use "$indexOfArray" expression to generate the sort field. There's an example here: kamsky.org/stupid-tricks-with-mongodb/…
@AsyaKamsky you should add that as an answer. I am sure people can miss your comment, and it's a pretty good solution.
4

Example of our collection:

{ "_id" : ObjectId("580e51fc87a0572ee623854f"), "name" : "Asya" }
{ "_id" : ObjectId("580e520087a0572ee6238550"), "name" : "Charlie" }
{ "_id" : ObjectId("580e520587a0572ee6238551"), "name" : "Tess" }
{ "_id" : ObjectId("580e520887a0572ee6238552"), "name" : "David" }
{ "_id" : ObjectId("580e520c87a0572ee6238553"), "name" : "Kyle" }
{ "_id" : ObjectId("580e521287a0572ee6238554"), "name" : "Aly" }

The query we want to run is one that will return all documents where name is one of "David", "Charlie" or "Tess" and we want them in that exact order.

> db.people.find({"name":{"$in": ["David", "Charlie", "Tess"]}}).sort({ ??? })

Let's define a variable called "order" so we don't have to keep typing the names in the array:

> order = [ "David", "Charlie", "Tess" ]

Here's how we can do this with aggregation framework:

​    m = { "$match" : { "name" : { "$in" : order } } };
    a = { "$addFields" : { "__order" : { "$indexOfArray" : [ order, "$name" ] } } };
    s = { "$sort" : { "__order" : 1 } };
    db.people.aggregate( [ m, a, s ] );

​Our result:

{ "_id" : ObjectId("580e520887a0572ee6238552"), "name" : "David", "__order" : 0 }
{ "_id" : ObjectId("580e520087a0572ee6238550"), "name" : "Charlie", "__order" : 1 }
{ "_id" : ObjectId("580e520587a0572ee6238551"), "name" : "Tess", "__order" : 2 }

The "$addFields" stage is new in 3.4 and it allows you to "$project" new fields to existing documents without knowing all the other existing fields. The new "$indexOfArray" expression returns position of particular element in a given array.

The result of this aggregation will be documents that match your condition, in order specified in the input array "order", and the documents will include all original fields, plus an additional field called "__order". If we want to remove this field, 3.4 allows "$project" stage with just exclusion specification, so we would just add { "$project": {"__order":0}} at the end of our pipeline.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.