如何在MongoDB的文档集合中对数组进行排序?

57

我有一组学生,每个学生都有一个类似以下样子的记录,我想按照分数score降序排序scores数组。

在Mongo shell上如何实现?

> db.students.find({'_id': 1}).pretty()
{
        "_id" : 1,
        "name" : "Aurelia Menendez",
        "scores" : [
                {
                        "type" : "exam",
                        "score" : 60.06045071030959
                },
                {
                        "type" : "quiz",
                        "score" : 52.79790691903873
                },
                {
                        "type" : "homework",
                        "score" : 71.76133439165544
                },
                {
                        "type" : "homework",
                        "score" : 34.85718117893772
                }
        ]
}

我正在尝试这个咒语....

 doc = db.students.find()

 for (_id,score) in doc.scores:
     print _id,score

但是它没有起作用。

17个回答

65
您需要在应用程序代码中或使用MongoDB 2.2中的新聚合框架中操作嵌入式数组。

mongo shell 中的聚合示例:

db.students.aggregate(
    // Initial document match (uses index, if a suitable one is available)
    { $match: {
        _id : 1
    }},

    // Expand the scores array into a stream of documents
    { $unwind: '$scores' },

    // Filter to 'homework' scores 
    { $match: {
        'scores.type': 'homework'
    }},

    // Sort in descending order
    { $sort: {
        'scores.score': -1
    }}
)

示例输出:

{
    "result" : [
        {
            "_id" : 1,
            "name" : "Aurelia Menendez",
            "scores" : {
                "type" : "homework",
                "score" : 71.76133439165544
            }
        },
        {
            "_id" : 1,
            "name" : "Aurelia Menendez",
            "scores" : {
                "type" : "homework",
                "score" : 34.85718117893772
            }
        }
    ],
    "ok" : 1
}

3
你可以修改聚合管道的末尾,以升序排序(最小值排在第一位),并限制只返回一个文档:{ $sort: { 'scores.score': 1 }}, { $limit : 1 } - Stennie
重复数据的问题,您将在每个对象中重复名称。因此,如果我在上层有20个字段,那么我应该重复吗? - Prabjot Singh
@PrabjotSingh 我并不完全清楚你的问题是什么,但是与其在评论中讨论,不如发布一个新问题,附上你的文件结构示例、期望输出以及MongoDB服务器/驱动程序的版本。 - Stennie
1
我同意@PrabjotSingh的观点,分数应该作为嵌入式数组返回?就像问题所建议的那样。 - F.O.O
@F.O.O 这个问题已经有6.5年的历史了,现在根据您使用的MongoDB服务器版本不同,有不同的选项可供选择。请发布一个新问题,并提供与您的环境和您试图解决的问题相关的详细信息。 - Stennie
然而,如果不破坏排序顺序,就无法倒带。 - Firoj Siddiki

14

Mongo 5.2 开始,这正是新的$sortArray 聚合操作符的确切用例:

// {
//   name: "Aurelia Menendez",
//   scores: [
//     { type: "exam",     score: 60.06 }
//     { type: "quiz",     score: 52.79 }
//     { type: "homework", score: 71.76 }
//     { type: "homework", score: 34.85 }
//   ]
// }
db.collection.aggregate([
  { $set: {
    scores: {
      $sortArray: {
        input: "$scores",
        sortBy: { score: -1 }
      }
    }
  }}
])
// {
//   name: "Aurelia Menendez",
//   scores: [
//     { type: "homework", score: 71.76 },
//     { type: "exam",     score: 60.06 },
//     { type: "quiz",     score: 52.79 },
//     { type: "homework", score: 34.85 }
//   ]
// }

这段代码会:

  • scores数组(input: "$scores")执行($sortArray)排序
  • 通过在score上应用排序(sortBy: { score: -1 })进行排序
  • 无需应用昂贵的$unwind$sort$group阶段的组合

如果只有一个数组,例如 [1,2,3,4,5] 呢? - Ryan Aquino
2
@RyanAquino 对整数数组进行排序 - Xavier Guihot
2
这个是给定问题的唯一正确答案。所有其他答案都使用不同的输入数据或更新现有数据或删除分数,而不是对数组进行排序或使用不同的语言,尽管它要求使用mongo shell。 - Wernfried Domscheit

8

由于这个问题可以以不同的方式管理,所以我想说另一种解决方案是“插入和排序”,这样你将在进行Find()操作时获得有序数组。

考虑以下数据:

{
   "_id" : 5,
   "quizzes" : [
      { "wk": 1, "score" : 10 },
      { "wk": 2, "score" : 8 },
      { "wk": 3, "score" : 5 },
      { "wk": 4, "score" : 6 }
   ]
}

在这里,我们将更新文档并进行排序。

db.students.update(
   { _id: 5 },
   {
     $push: {
       quizzes: {
          $each: [ { wk: 5, score: 8 }, { wk: 6, score: 7 }, { wk: 7, score: 6 } ],
          $sort: { score: -1 },
          $slice: 3 // keep the first 3 values
       }
     }
   }
)

结果是:

{
  "_id" : 5,
  "quizzes" : [
     { "wk" : 1, "score" : 10 },
     { "wk" : 2, "score" : 8 },
     { "wk" : 5, "score" : 8 }
  ]
}

Documentation: https://docs.mongodb.com/manual/reference/operator/update/sort/#up._S_sort


我们可以在存储的数组字段上使用$each吗? - Alok Deshwal
这将更新现有数据,在问题中只要求查询数据。 - Wernfried Domscheit

5
这就是我们用JS和mongo控制台解决问题的方式:
db.students.find({"scores.type": "homework"}).forEach(
  function(s){
    var sortedScores = s.scores.sort(
      function(a, b){
        return a.score<b.score && a.type=="homework";
      }
    );
    var lowestHomeworkScore = sortedScores[sortedScores.length-1].score;
    db.students.update({_id: s._id},{$pull: {scores: {score: lowestHomeworkScore}}}, {multi: true});
  })

4
伙计?你破坏了乐趣。 - markphd
find() 函数内的 {"scores.type": "homework"} 过滤表达式有什么作用吗? - Treefish Zhang
@TreefishZhang 为什么不行呢? - Aleksandr Panasyuk
@AlexanderPanasyuk 它取得了什么成果?-它是否筛选掉了一些学生? - Treefish Zhang
这将会更新现有数据,但问题只要求查询数据。使用 forEach 循环逐个更新文档通常会导致性能不佳。在主键 _id 上进行过滤时,{multi: true} 没有意义。 - Wernfried Domscheit

3
为了对数组进行排序,请按照以下步骤操作:
1)使用unwind迭代数组
2)对数组进行排序
3)使用group将数组对象合并为一个数组
4)然后投影其他字段
查询
db.taskDetails.aggregate([
    {$unwind:"$counter_offer"},
    {$match:{_id:ObjectId('5bfbc0f9ac2a73278459efc1')}},
    {$sort:{"counter_offer.Counter_offer_Amount":1}},
   {$unwind:"$counter_offer"},
   {"$group" : {_id:"$_id",
    counter_offer:{ $push: "$counter_offer" },
    "task_name": { "$first": "$task_name"},
    "task_status": { "$first": "$task_status"},
    "task_location": { "$first": "$task_location"},
}}

]).pretty()

$addToSet相反,使用$push保留了数组的顺序,按照前面的步骤排序。 - Alexander
按要求工作。但我不需要再解开。 - Utkarsh

2

以下是可用于查找数组中最低分并将其删除的Java代码。

public class sortArrayInsideDocument{
public static void main(String[] args) throws UnknownHostException {
    MongoClient client = new MongoClient();
    DB db = client.getDB("school");
    DBCollection lines = db.getCollection("students");
    DBCursor cursor = lines.find();
    try {
        while (cursor.hasNext()) {
            DBObject cur = cursor.next();
            BasicDBList dbObjectList = (BasicDBList) cur.get("scores");
            Double lowestScore = new Double(0);
            BasicDBObject dbObject = null;
            for (Object doc : dbObjectList) {
                BasicDBObject basicDBObject = (BasicDBObject) doc;
                if (basicDBObject.get("type").equals("homework")) {
                    Double latestScore = (Double) basicDBObject
                            .get("score");
                    if (lowestScore.compareTo(Double.valueOf(0)) == 0) {
                        lowestScore = latestScore;
                        dbObject = basicDBObject;

                    } else if (lowestScore.compareTo(latestScore) > 0) {
                        lowestScore = latestScore;
                        dbObject = basicDBObject;
                    }
                }
            }
            // remove the lowest score here.
            System.out.println("object to be removed : " + dbObject + ":"
                    + dbObjectList.remove(dbObject));
            // update the collection
            lines.update(new BasicDBObject("_id", cur.get("_id")), cur,
                    true, false);
        }
    } finally {
        cursor.close();
    }
}
}

1
不错!很好的例子...使用Java 8,我们可以最小化比较部分。 - Pinaki Mukherjee
@Vel 从dbObjectList中移除dbObject会如何从cur的DBObject中移除?curdbObjectList之间有什么联系? - user2761431

0
“虽然猜测起来很容易,但是请尽量不要通过作弊来完成Mongo大学的课程,否则你将无法理解基础知识。”
db.students.find({}).forEach(function(student){ 

    var minHomeworkScore,  
        scoresObjects = student.scores,
        homeworkArray = scoresObjects.map(
            function(obj){
                return obj.score;
            }
        ); 

    minHomeworkScore = Math.min.apply(Math, homeworkArray);

    scoresObjects.forEach(function(scoreObject){ 
        if(scoreObject.score === minHomeworkScore){ 
            scoresObjects.splice(scoresObjects.indexOf(minHomeworkScore), 1); 
        } 
    });

    printjson(scoresObjects);

});

0

对订单标题和数组标题进行排序,并返回整个集合数据。集合名称为menu

[
            {
                "_id": "5f27c5132160a22f005fd50d",
                "title": "Gift By Category",
                "children": [
                    {
                        "title": "Ethnic Gift Items",
                        "s": "/gift?by=Category&name=Ethnic"
                    },
                    {
                        "title": "Novelty Gift Items",
                        "link": "/gift?by=Category&name=Novelty"
                    }
                ],
                "active": true
            },
            {
                "_id": "5f2752fc2160a22f005fd50b",
                "title": "Gift By Occasion",
                "children": [
                    {
                        "title": "Gifts for Diwali",
                        "link": "/gift-for-diwali" 
                    },
                    {
                        "title": "Gifts for Ganesh Chathurthi",
                        "link": "/gift-for-ganesh-chaturthi",
                    }
                ],
                
                "active": true
            }
    ]

以下是查询语句。
let menuList  = await  Menu.aggregate([
                { 
                    $unwind: '$children'
                }, 
                {
                    $sort:{"children.title":1}
                },
                {   
                    $group : { _id : "$_id",
                        root: { $mergeObjects: '$$ROOT' },   
                        children: { $push: "$children" } 
                    } 
                },
                {
                    $replaceRoot: {
                        newRoot: {
                            $mergeObjects: ['$root', '$$ROOT']
                        }
                    }
                },
                {
                    $project: {
                        root: 0 
                    }
                },
                { 
                    $match: {
                                $and:[{'active':true}],
                            }
                },
                {
                    $sort:{"title":1}
                }                  
    ]);

0

在问题被编辑之前,它是这样的

我想按照“类型”:“作业”的分数降序排序。

这意味着,数组必须经过过滤和排序。在MongoDB的更新版本中,您可以像这样做:

db.collection.aggregate([
   {
      $set: {
         scores: {
            $sortArray: {
               input: {
                  $filter: {
                     input: "$scores",
                     cond: { $eq: ["$$this.type", "homework"] }
                  }
               },
               sortBy: { score: -1 }
            }
         }
      }
   }
])

-1

我相信你正在学习 M101P: MongoDB for Developers,其中作业3.1是要从两个作业分数中删除较低的一个。由于在那个时候还没有教授聚合操作,你可以尝试以下方法:

import pymongo

conn = pymongo.MongoClient('mongodb://localhost:27017')
db = conn.school
students = db.students

for student_data in students.find():
    smaller_homework_score_seq = None
    smaller_homework_score_val = None
    for score_seq, score_data in enumerate(student_data['scores']):
        if score_data['type'] == 'homework':
            if smaller_homework_score_seq is None or smaller_homework_score_val > score_data['score']:
                smaller_homework_score_seq = score_seq
                smaller_homework_score_val = score_data['score']
    students.update({'_id': student_data['_id']}, {'$pop': {'scores': smaller_homework_score_seq}})

OP 是针对mongo js shell的,但这个Python示例非常简洁! - charles ross

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接