mongodb - MongoDB:对嵌入式文档使用“或”运算符对查询进行排序和限制

标签 mongodb mongodb-query aggregation-framework

具有以下数据结构:

"feature": {
  "site": {
    "subjects": [ 
      {                   
        "subject_id" : 1,
        "time" : ISODate("2014-06-28T06:38:29.751Z")
      }
    ],
  },
  "mobile": {            
    "subjects" : [ 
      {                    
        "subject_id" : 1,
        "time" : ISODate("2014-06-28T16:14:29.758Z")
      }, 
      {                    
        "subject_id" : 2,
        "time" : ISODate("2014-06-24T23:44:29.759Z")
      }
    ]
  }
}


我希望进行查询以获取在“移动”或“网站”中嵌入了ID为1的主题的所有功能。使用此查询:

db.features.find( { $or: [ { site.subjects.subject_id: 1 }, { mobile.subjects.subject_id:1 } ] } )


如何通过(移动或网站).subjects.time对此类查询进行排序?

最佳答案

关于“排序”问题的一般情况是,需要在其上进行排序的“特定”字段值。通过在创建或更新文档时包含该字段,实际上可以获得最佳性能。您不能单独使用查找来“有条件地排序”。

如果您需要“动态”地进行操作,那么在这种情况下,您希望“投影”符合您条件的内容,为此,您需要聚合框架。

这里有一些陷阱,因为对文档进行操作以达到此目的时,操作过程不像一般的查询逻辑那样宽容。通常,在处理数组时,需要确保在使用它们时没有空内容。根据您的样本数据,一些额外的样本为解决问题提供了指南:

{
    "_id" : ObjectId("53b49853c1a7b867c4541482"),
    "site" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T06:38:29.751Z")
            }
        ]
    },
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T16:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}
{
    "_id" : ObjectId("53b4ccb6fbc9071ff8fc2d5b"),
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T16:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}
{
    "_id" : ObjectId("53b4cf58c4e3a228da24c225"),
    "site" : {
        "subjects" : [
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-28T06:38:29.751Z")
            }
        ]
    },
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T16:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}
{
    "_id" : ObjectId("53b4d03bc4e3a228da24c227"),
    "site" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T18:38:29.751Z")
            }
        ]
    },
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T04:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}


第一个文档是您的基本样本,而另一个文档则出于特定目的而在某些方面有所不同,以展示一些可能的问题,当然不一定表示您自己的数据。

第二个文档故意省略了“ site”键,而第三个文档虽然存在“ site”,但“ subject_id”将不符合要考虑的条件。是的,这是选择文档的$or条件,但是我们在这里进一步考虑仅考虑那些也符合条件的“子文档”元素。此处的意思是,要对内容进行排序甚至是“过滤”的内容的“日期”将不考虑其中任何不具有必需的"subject_id": 1的项目。

首先查看仅创建一个可以根据条件进行排序的值:

db.features.aggregate([
    { "$match": {
         "$or": [ 
             { "site.subjects.subject_id": 1 }, 
             { "mobile.subjects.subject_id": 1 }
         ]
    }},
    { "$project": {
        "site": 1,
        "mobile": 1,
        "scopy": { "$ifNull": ["$site.subjects", { "$const": [false] }] },
        "mcopy": { "$ifNull": ["$mobile.subjects", { "$const": [false] }] }
    }},
    { "$unwind": "$scopy" },
    { "$project": {
        "site": 1,
        "mobile": 1,
        "scopy": {
            "$cond": [
                { "$eq": [ "$scopy.subject_id", 1 ] },
                "$scopy.time",
                false
            ]
        },
        "mcopy": 1
    }},
    { "$sort": { "_id": 1, "scopy": -1 } },
    { "$group": {
        "_id": "$_id",
        "site": { "$first": "$site" },
        "mobile": { "$first": "$mobile" },
        "mcopy": { "$first": "$mcopy" },
        "scopy": { "$first": "$scopy" }
    }},
    { "$unwind": "$mcopy" },
    { "$project": {
        "site": 1,
        "mobile": 1,
        "scopy": 1,
        "mcopy": {
            "$cond": [
                { "$eq": [ "$mcopy.subject_id", 1 ] },
                "$mcopy.time",
                false
            ]
        }
    }},
    { "$sort": { "_id": 1, "mcopy": -1 } },
    { "$group": {
        "_id": "$_id",
        "site": { "$first": "$site" },
        "mobile": { "$first": "$mobile" },
        "mcopy": { "$first": "$mcopy" },
        "scopy": { "$first": "$scopy" }
    }},
    { "$project": {
        "site": { 
            "$ifNull": [ 
                "$site", 
                { "$const": { "subjects": [] } }
            ]
        },
        "mobile": {
            "$ifNull": [
                "$mobile",
                { "$const": { "subjects": [] } }
            ]
        },
        "best": { 
            "$cond": [
                { "$gt": [ "$mcopy", "$scopy" ] },
                "$mcopy",
                "$scopy"
            ]
        }
    }},
    { "$sort": { "best": -1 } },
    { "$project": {
        "site": 1,
        "mobile": 1
    }}
])


这应该从具有最新值的“站点”开始,优先选择“时间”值上的文档。样本中的最后一个文档应首先显示。

现在,如果您实际上是在标题中要求“限制”,我认为这意味着对实际的“匹配”结果进行“过滤”,那么您所做的工作就略有不同:

db.features.aggregate([
    { "$match": {
         "$or": [ 
             { "site.subjects.subject_id": 1 }, 
             { "mobile.subjects.subject_id": 1 }
         ]
    }},
    { "$project": {
        "wsite": { "$ifNull": ["$site.subjects", { "$const": [false] }] },
        "wmobile": { "$ifNull": ["$mobile.subjects", { "$const": [false] }] }
    }},
    { "$unwind": "$wsite" },
    { "$project": {
        "wsite": {
            "$cond": [
                { "$eq": [ "$wsite.subject_id", 1 ] },
                "$wsite",
                false
            ]
        },
        "wmobile": 1
    }},
    { "$group": {
        "_id": "$_id",
        "wsite": { "$addToSet": "$wsite" },
        "wmobile": { "$first": "$wmobile" },
        "msite": { "$max": "$wsite.time" },
        "csite": { "$sum": 1 }
    }},
    { "$unwind": "$wsite" },
    { "$match": {
        "$or": [ 
            { "wsite": { "$ne": false } },
            { "csite": 1 }
        ]
    }},
    { "$group": {
        "_id": "$_id",
        "wsite": { "$push": "$wsite" },
        "wmobile": { "$first": "$wmobile" },
        "msite": { "$first": "$msite" }
    }},
    { "$unwind": "$wmobile" },
    { "$project": {
        "wsite": 1,
        "wmobile": {
            "$cond": [
                { "$eq": [ "$wmobile.subject_id", 1 ] },
                "$wmobile",
                false
            ]
        },
        "msite": 1,
    }},
    { "$group": {
        "_id": "$_id",
        "wsite": { "$first": "$wsite" },
        "wmobile": { "$addToSet": "$wmobile" },
        "msite": { "$first": "$msite" },
        "mmobile": { "$max": "$wmobile.time" },
        "cmobile": { "$sum": 1 }
    }},
    { "$unwind": "$wmobile" },
    { "$match": { 
        "$or": [
            { "wmobile": { "$ne": false } },
            { "cmobile": 1 }
        ]
    }},
    { "$group": {
        "_id": "$_id",
        "wsite": { "$first": "$wsite" },
        "wmobile": { "$push": "$wmobile" },
        "msite": { "$first": "$msite" },
        "mmobile": { "$first": "$mmobile" }
    }},
    { "$project": {
        "site": {
            "subjects": {
                "$cond": [
                    { "$eq": [ "$wsite", { "$const": [false] } ] },
                    { "$const": [] },
                    "$wsite"
                ]
            }
        },
        "mobile": {
            "subjects": {
                "$cond": [
                    { "$eq": [ "$wmobile", { "$const": [false] } ] },
                    { "$const": [] },
                    "$wmobile"
                ]
            }
        },
        "best": { 
            "$cond": [
                { "$gt": [ "$mmobile", "$msite" ] },
                "$mmobile",
                "$msite"
            ]
        }
    }},
    { "$sort": { "best": -1 } },
    { "$project": {
        "site": 1,
        "mobile": 1
    }}
])


具有MongoDB 2.6中的功能的清洁器,其中大多数阵列过滤可以在一个阶段内完成:

db.features.aggregate([
    { "$match": {
         "$or": [ 
             { "site.subjects.subject_id": 1 }, 
             { "mobile.subjects.subject_id": 1 }
         ]
    }},
    { "$project": {
        "wsite": {
            "$let": {
                "vars": {            
                    "list": { "$setDifference": [
                        {
                            "$map": {
                                 "input": {
                                     "$ifNull": [ 
                                         "$site.subjects",
                                         { "$literal": [false] }
                                     ]
                                 },
                                 "as": "el",
                                 "in": {
                                     "$cond": [
                                         { "$eq": [ "$$el.subject_id", 1 ] },
                                         "$$el",
                                         false
                                     ]
                                 }
                             }
                        },
                        [false]
                    ]}
                },
                "in": {
                    "$cond": [
                        { "$eq": [{ "$size": "$$list" }, 0 ] },
                        { "$literal": [false] },
                        "$$list"
                    ]
                }
            }    
        },
        "wmobile": {
            "$let": {
                "vars": {            
                    "list": { "$setDifference": [
                        {
                            "$map": {
                                 "input": {
                                     "$ifNull": [ 
                                         "$mobile.subjects",
                                         { "$literal": [false] }
                                     ]
                                 },
                                 "as": "el",
                                 "in": {
                                     "$cond": [
                                         { "$eq": [ "$$el.subject_id", 1 ] },
                                         "$$el",
                                         false
                                     ]
                                 }
                             }
                        },
                        [false]
                    ]}
                },
                "in": {
                    "$cond": [
                        { "$eq": [{ "$size": "$$list" }, 0 ] },
                        { "$literal": [false] },
                        "$$list"
                    ]
                }
            }    
        }
    }},
    { "$unwind": "$wsite" },
    { "$group": {
        "_id": "$_id",
        "wsite": { "$push": "$wsite" },
        "wmobile": { "$first": "$wmobile" },
        "fsite": { "$max": "$wsite.time" }
    }},
    { "$unwind": "$wmobile" },
    { "$group": {
        "_id": "$_id",
        "wsite": { "$first": "$wsite" },
        "wmobile": { "$push": "$wmobile" },
        "fsite": { "$first": "$fsite" },
        "fmobile": { "$max": "$wmobile.time" }
    }},
    { "$project": {
        "site": {
            "subjects": {
                "$cond": [
                    { "$allElementsTrue": "$wsite" },
                    "$wsite",
                    { "$literal": [] }
                ]
            }
        },
        "mobile": {
            "subjects": {
                "$cond": [
                    { "$allElementsTrue": "$wmobile" },
                    "$wmobile",
                    { "$literal": [] }
                ]
            }
        },
        "best": {
            "$cond": [
                { "$gt": [ "$fmobile", "$fsite" ] },
                "$fmobile",
                "$fsite"
            ]
        }
    }},
    { "$sort": { "best": -1 } },
    { "$project": {
        "site": 1,
        "mobile": 1
    }}
])


这些语句中要考虑的主要事项是数组的处理。如果不存在实际的数组,此处“要求”输入数组的各种操作将失败。更糟糕的情况是,当涉及到$unwind时,如果呈现一个完全“空”的数组,它将认为该文件没有“扩展”的作用,只会从管道中完全删除该文档。

主要的“计数器”是$ifNull。这实际上测试了字段的“存在”,并返回它或作为第二个参数的替代结果。每种情况都使用此方法返回带有单个元素[false]的数组,这意味着任何后续的$unwind不仅不会由于缺少作为数组的字段而“爆炸”,而且也不会考虑当前文档为空,因此将其删除。

    { "$project": {
        "site": 1,
        "mobile": 1,
        "scopy": { "$ifNull": ["$site.subjects", { "$const": [false] }] },
        "mcopy": { "$ifNull": ["$mobile.subjects", { "$const": [false] }] }
    }},


第一个示例保留原始字段,因为在确定文档的排序方式之后,它们将“按原样”返回。但是,与副本一样,或者仅对匹配结果进行“过滤”,将以某种方式操纵这些结果以“过滤”并确定用于排序的日期。

在不更改现有阵列的情况下,第一个示例相对简单。在这里,您要做的基本上是对文档中的数组进行“排序”,展开后每次一次获取最新的日期。

    { "$unwind": "$mcopy" },
    { "$project": {
        "site": 1,
        "mobile": 1,
        "scopy": 1,
        "mcopy": {
            "$cond": [
                { "$eq": [ "$mcopy.subject_id", 1 ] },
                "$mcopy.time",
                false
            ]
        }
    }},
    { "$sort": { "_id": 1, "mcopy": -1 } },


此版本中要做的另一件事是确保要考虑的日期来自与条件匹配的“子文档”。如果不是,则将日期替换为false,该日期将排在列表的底部。

然后,此处的$group使用$first运算符在排序后拾取最近的项目。现在,对每个数组执行该过程会给出两个日期进行比较,因此您可以决定最后对哪个日期进行排序。

    { "$group": {
        "_id": "$_id",
        "site": { "$first": "$site" },
        "mobile": { "$first": "$mobile" },
        "mcopy": { "$first": "$mcopy" },
        "scopy": { "$first": "$scopy" }
    }},


在“过滤”方法中,不仅进行比较以查看所考虑的“日期”是否符合条件,而且实际上会考虑并删除整个“子文档”元素(如果不匹配)。

这里要注意不要完全“销毁”文档,也不要留下空的数组,或者如果该数组中的任何内容都不匹配,则不要删除该文档。这说明了使用$unwind然后使用$project进行比较以及接下来匹配结果的“大小”的过程。

这些可以使用$group运算符放入$addToSet中,因为您可以合理地假定结果是不合理的,并且在这种情况下还可以使用$max来找到最大的“日期”值。这还将任何false值压缩为一个条目。

    { "$group": {
        "_id": "$_id",
        "wsite": { "$addToSet": "$wsite" },
        "wmobile": { "$first": "$wmobile" },
        "msite": { "$max": "$wsite.time" },
        "csite": { "$sum": 1 }
    }},


只有这样,您才能再次$unwind并安全地使用$match过滤掉false之前的所有内容。如果实际上在该数组中只有一个false值,请注意不要删除文档。现在,最后的“分组”应该在每个数组下具有过滤的结果或仅false的单个值。

    { "$unwind": "$wsite" },
    { "$match": {
        "$or": [ 
            { "wsite": { "$ne": false } },
            { "csite": 1 }
        ]
    }},
    { "$group": {
        "_id": "$_id",
        "wsite": { "$push": "$wsite" },
        "wmobile": { "$first": "$wmobile" },
        "msite": { "$first": "$msite" }
    }},


在最终清单中,我们将利用可以在MongoDB 2.6中实现的功能完成的新工作。

由于新的$map运算符允许在不使用$unwind的情况下进行某些数组处理,因此将“清单”中的各个流水线阶段“组合”在一起。基本上,对匹配条件进行相同的评估,并且通过将false与仅包含$setDifference的数组进行比较来“过滤掉”返回的[false]值。

然后使用$size运算符测试不包含任何匹配项的任何“空”数组,其中的empty将返回0的大小。然后,这里的条件只是像以前一样用单个[false]替换那些空数组。

最后一部分的原因是,您仍然需要$unwind才能从每个数组中获取最大或$max“日期”值。

    { "$unwind": "$wsite" },
    { "$group": {
        "_id": "$_id",
        "wsite": { "$push": "$wsite" },
        "wmobile": { "$first": "$wmobile" },
        "fsite": { "$max": "$wsite.time" }
    }},


从这里开始,不同的编码方式几乎是相似的。现在您有了可以与每个数组进行比较的日期,您只需确定哪个是最新的或其他逻辑比较:

        "best": {
            "$cond": [
                { "$gt": [ "$fmobile", "$fsite" ] },
                "$fmobile",
                "$fsite"
            ]
        }
    }},
    { "$sort": { "best": -1 } },
    { "$project": {
        "site": 1,
        "mobile": 1
    }}


然后将结果日期值用于$sort最终结果,然后将其传递给$project以删除我们的计划字段以进行日期比较。

在任何一种情况下,通过比较示例文档得出的结果顺序为“第四”,“第一”,“第二”和“第三”。 “第四”文档在首选“站点”字段上具有最新日期,因此它是最佳结果。 “第一个”样本具有将要选择的下一个最大日期。

“第二”和“第三”实际上选择了相同的日期值,即使两者都没有“站点”字段可能的匹配条目。此处排序的唯一原因实际上只是文档_id值,这是文档进入管道的方式。

如果不“过滤”数组,则输出实际为:

{
    "_id" : ObjectId("53b4d03bc4e3a228da24c227"),
    "site" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T18:38:29.751Z")
            }
        ]
    },
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T04:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}
{
    "_id" : ObjectId("53b4cf58c4e3a228da24c225"),
    "site" : {
        "subjects" : [
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-28T06:38:29.751Z")
            }
        ]
    },
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T16:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}
{
    "_id" : ObjectId("53b4ccb6fbc9071ff8fc2d5b"),
    "site" : {
        "subjects" : [ ]
    },
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T16:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}
{
    "_id" : ObjectId("53b49853c1a7b867c4541482"),
    "site" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T06:38:29.751Z")
            }
        ]
    },
    "mobile" : {
        "subjects" : [
            {
                "subject_id" : 1,
                "time" : ISODate("2014-06-28T16:14:29.758Z")
            },
            {
                "subject_id" : 2,
                "time" : ISODate("2014-06-24T23:44:29.759Z")
            }
        ]
    }
}


并进行过滤:

{
    "_id" : ObjectId("53b4d03bc4e3a228da24c227"),
    "site" : {
        "subjects" : [
             {
                 "subject_id" : 1,
                 "time" : ISODate("2014-06-28T18:38:29.751Z")
             }
        ]
    },
    "mobile" : {
        "subjects" : [
             {
                 "subject_id" : 1,
                 "time" : ISODate("2014-06-28T04:14:29.758Z")
             }
        ]
    }
}
{
    "_id" : ObjectId("53b49853c1a7b867c4541482"),
    "site" : {
        "subjects" : [
             {
                 "subject_id" : 1,
                 "time" : ISODate("2014-06-28T06:38:29.751Z")
             }
        ]
    },
    "mobile" : {
        "subjects" : [
             {
                 "subject_id" : 1,
                 "time" : ISODate("2014-06-28T16:14:29.758Z")
             }
        ]
    }
}
{
    "_id" : ObjectId("53b4ccb6fbc9071ff8fc2d5b"),
    "site" : {
        "subjects" : [ ]
    },
    "mobile" : {
        "subjects" : [
             {
                 "subject_id" : 1,
                 "time" : ISODate("2014-06-28T16:14:29.758Z")
             }
        ]
    }
}
{
    "_id" : ObjectId("53b4cf58c4e3a228da24c225"),
    "site" : {
        "subjects" : [ ]
    },
    "mobile" : {
        "subjects" : [
             {
                 "subject_id" : 1,
                 "time" : ISODate("2014-06-28T16:14:29.758Z")
             }
        ]
    }
}




这里的主要情况是,虽然可以像这样“投影”一个字段进行比较,但是通常最好将其保留在文档中,因为这样您就可以快速进行排序,而无需为每个文档先构建一个开销。

如果确实需要将数组结果“过滤”为与条件匹配的结果,那么您确实会这样做,因为位置$运算符可用的投影将不支持与“两个”数组的匹配。

无论如何,至少这是使用聚合框架更高级地使用文档“重塑”的示例,并在那里显示了可能性。但是,就像所有复杂的操作一样,这确实需要付出一定的代价,因此,在性能方面,您应该围绕此设计数据。

关于mongodb - MongoDB:对嵌入式文档使用“或”运算符对查询进行排序和限制,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24531840/

相关文章:

MongoDB 显示一对多关系中的子项目

node.js - 如何从 Mongoose 的填充模型中查找文本?

mongodb - 尝试使用Grails 2.3.7查询MongoDB域类时出现IllegalStateException

node.js - Mongoose 试图打开未关闭的连接

javascript - Mongoose 查找/更新子文档

mongodb - 如何将两个数组转换为 mongoDB 中的对象,其中第一个数组具有多个相同的值

mongodb - 在一个 MongoDB 聚合查询中进行排序和分组

node.js - 我如何为多个集合做 Mongodb 聚合过滤器?

node.js - 如何在 Mongoose 中使用聚合

MongoDB:在聚合第二个匹配失败后保留第一个匹配的结果