我正在尝试对示例bios集合进行查询http://docs.mongodb.org/manual/reference/bios-example-collection/:
检索所有人和他们在获得图灵奖之前获得的奖项
我提出了这个问题:
db.bios.aggregate([
{$match: {"awards.award" : "Turing Award"}},
{$project: {"award1": "$awards", "award2": "$awards", "first_name": "$name.first", "last_name": "$name.last"}},
{$unwind: "$award1"},
{$match: {"award1.award" : "Turing Award"}},
{$unwind: "$award2"},
{$redact: {
$cond: {
if: { $eq: [ { $gt: [ "$award1.year", "$award2.year"] }, true]},
then: "$$KEEP",
else: "$$PRUNE"
}
}
}
])
这就是答案:
/* 0 */
{
"result" : [
{
"_id" : 1,
"award1" : {
"award" : "Turing Award",
"year" : 1977,
"by" : "ACM"
},
"award2" : {
"award" : "W.W. McDowell Award",
"year" : 1967,
"by" : "IEEE Computer Society"
},
"first_name" : "John",
"last_name" : "Backus"
},
{
"_id" : 1,
"award1" : {
"award" : "Turing Award",
"year" : 1977,
"by" : "ACM"
},
"award2" : {
"award" : "National Medal of Science",
"year" : 1975,
"by" : "National Science Foundation"
},
"first_name" : "John",
"last_name" : "Backus"
},
{
"_id" : 4,
"award1" : {
"award" : "Turing Award",
"year" : 2001,
"by" : "ACM"
},
"award2" : {
"award" : "Rosing Prize",
"year" : 1999,
"by" : "Norwegian Data Association"
},
"first_name" : "Kristen",
"last_name" : "Nygaard"
},
{
"_id" : 5,
"award1" : {
"award" : "Turing Award",
"year" : 2001,
"by" : "ACM"
},
"award2" : {
"award" : "Rosing Prize",
"year" : 1999,
"by" : "Norwegian Data Association"
},
"first_name" : "Ole-Johan",
"last_name" : "Dahl"
}
],
"ok" : 1
}
我不喜欢这个解决方案的地方是我打开了$award2
。相反,我很乐意保留award2作为一个数组,只删除award1之后收到的奖项。因此,例如,约翰·巴克斯的答案应该是:
{
"_id" : 1,
"first_name" : "John",
"last_name" : "Backus",
"award1" : {
"award" : "Turing Award",
"year" : 1977,
"by" : "ACM"
},
"award2" : [
{
"award" : "W.W. McDowell Award",
"year" : 1967,
"by" : "IEEE Computer Society"
},
{
"award" : "National Medal of Science",
"year" : 1975,
"by" : "National Science Foundation"
}
]
}
有没有可能用$redact
而不做$unwind: "$award2"
来实现它?
如果你在问题中以文档的原始状态为例,这可能会更有帮助,因为这清楚地显示了"你来自哪里",然后将"你想去哪里"作为目标,以及你想要的输出。
这只是一个提示,但看起来你是从这样一个文档开始的:
{
"_id" : 1,
"name": {
"first" : "John",
"last" : "Backus"
},
"awards" : [
{
"award" : "W.W. McDowell Award",
"year" : 1967,
"by" : "IEEE Computer Society"
},
{
"award" : "National Medal of Science",
"year" : 1975,
"by" : "National Science Foundation"
},
{
"award" : "Turing Award",
"year" : 1977,
"by" : "ACM"
},
{
"award" : "Some other award",
"year" : 1979,
"by" : "Someone Else"
}
]
}
因此,这里真正的要点是,虽然您可能已经在这里找到了$redact
(这比使用$project
来处理逻辑条件,然后使用$match
来过滤逻辑匹配要好一点),但这可能不是您想要在这里进行比较的最佳工具。
在继续之前,我只想指出$redact
的主要问题。无论你在这里做什么,逻辑(没有展开)本质上都是在$$DESCEND
上"直接"比较,以便在任何级别处理值为"年"的数组元素。
这种递归也会使"award1"条件无效,因为它具有相同的字段名。即使重命名该字段也会杀死逻辑,因为缺少该字段的投影值不会大于测试值。
简而言之,$redact
被排除在外,因为你不能用它所应用的逻辑说"只从这里获取"。
另一种方法是使用$map
和$setDifference
过滤阵列中的内容,如下所示:
db.bios.aggregate([
{ "$match": { "awards.award": "Turing Award" } },
{ "$project": {
"first_name": "$name.first",
"last_name": "$name.last",
"award1": { "$setDifference": [
{ "$map": {
"input": "$awards",
"as": "a",
"in": { "$cond": [
{ "$eq": [ "$$a.award", "Turing Award" ] },
"$$a",
false
]}
}},
[false]
]},
"award2": { "$setDifference": [
{ "$map": {
"input": "$awards",
"as": "a",
"in": { "$cond": [
{ "$ne": [ "$$a.award", "Turing Award" ] },
"$$a",
false
]}
}},
[false]
]}
}},
{ "$unwind": "$award1" },
{ "$project": {
"first_name": 1,
"last_name": 1,
"award1": 1,
"award2": { "$setDifference": [
{ "$map": {
"input": "$award2",
"as": "a",
"in": { "$cond": [
{ "$gt": [ "$award1.year", "$$a.year" ] },
"$$a",
false
]}
}},
[false]
]}
}}
])
实际上,无论是在迭代阶段使用$unwind
,还是在这里使用第二个$project
,都没有"漂亮"的方法,因为$map
(和$setDifference
过滤器)返回的是"仍然是一个数组"。因此,$unwind
对于使"数组"成为一个单数(前提是您的条件仅与1个元素匹配)条目是必要的,以便在比较中使用。
试图在单个$project
中"压缩"所有逻辑只会在第二个输出中产生"数组",因此仍然需要一些"展开",但至少这样展开(希望)1匹配的成本并没有那么高,并且可以保持输出的干净。
但这里需要注意的另一件事是,你根本没有真正"聚合"任何东西。这只是文档操作,所以您可以考虑直接在客户端代码中进行操作。如这个shell示例所示:
db.bios.find(
{ "awards.award": "Turing Award" },
{ "name": 1, "awards": 1 }
).forEach(function(doc) {
doc.first_name = doc.name.first;
doc.last_name = doc.name.last;
doc.award1 = doc.awards.filter(function(award) {
return award.award == "Turing Award"
})[0];
doc.award2 = doc.awards.filter(function(award) {
return doc.award1.year > award.year;
});
delete doc.name;
delete doc.awards;
printjson(doc);
})
无论如何,两种方法都会输出相同的
{
"_id" : 1,
"first_name" : "John",
"last_name" : "Backus",
"award1" : {
"award" : "Turing Award",
"year" : 1977,
"by" : "ACM"
},
"award2" : [
{
"award" : "W.W. McDowell Award",
"year" : 1967,
"by" : "IEEE Computer Society"
},
{
"award" : "National Medal of Science",
"year" : 1975,
"by" : "National Science Foundation"
}
]
}
这里唯一真正的区别是,通过使用.aggregate()
,"award2"的内容在从服务器返回时已经被过滤,这可能与客户端处理方法没有太大区别,除非将被删除的项目包括每个文档一个相当大的列表。
对于记录,这里真正需要对现有聚合管道进行的唯一更改是在末尾添加$group
,以将数组条目"重新组合"到单个文档中:
db.bios.aggregate([
{ "$match": { "awards.award": "Turing Award" } },
{ "$project": {
"first_name": "$name.first",
"last_name": "$name.last",
"award1": "$awards",
"award2": "$awards"
}},
{ "$unwind": "$award1" },
{ "$match": {"award1.award" : "Turing Award" }},
{ "$unwind": "$award2" },
{ "$redact": {
"$cond": {
"if": { "$gt": [ "$award1.year", "$award2.year"] },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$group": {
"_id": "$_id",
"first_name": { "$first": "$first_name" },
"last_name": { "$first": "$last_name" },
"award1": { "$first": "$award1" },
"award2": { "$push": "$award2" }
}}
])
但话说回来,这里的所有操作都有"阵列复制"one_answers"展开成本"。因此,为了避免这种情况,前两种方法中的任何一种都是你真正想要的。
您可以使用带有嵌套表达式的单个项目阶段来避免多个阶段:
db.bios.aggregate([
{$match : {"awards.award" : "Turing Award"}},
{$project : {
award1 : { $arrayElemAt : [{
$filter : {
input : "$awards",
as : "award",
cond : {$eq : ["$$award.award","Turing Award"]}
}}, 0]},
award2 : { $let : {
vars : {
turing_year : { $let : {
vars : {
turingAward :{"$arrayElemAt" : [{"$filter" : {
input : "$awards",
as : "award",
cond : {$eq : ["$$award.award","Turing Award"]}
}}, 0]}},
in : "$$turingAward.year"}}},
in : {
$filter : {
input : "$awards",
as : "award",
cond : {$lt : ["$$award.year", "$$turing_year"]}
}
}
}},
first_name : "$name.first",
last_name : "$name.last"}
}]).pretty();
请查看此处的文档以获取一组有用的数组运算符。
然而,聚合对于这个查询来说并不好看,而且逻辑足够简单,可以在代码本身中实现,不会对性能产生太大影响;只是同意Blakes Seven的回答。但是,MongoDB的一个巧妙之处是,我们可以设计模式来支持我们的访问模式,并保持代码的整洁。如果在实际场景中需要这样的功能,我们可以简单地在文档中包含一个名为"turing_award_year"的字段。这将影响集合上的CRUD操作,但代码将是干净的,现在我们可以使用这样一个非常容易维护的查询:
db.bios.aggregate(
[
{$match : {"awards.award" : "Turing Award"}},
{$project : {
award1 : { $arrayElemAt : [{
$filter : {
input : "$awards",
as : "award",
cond : {$eq : ["$$award.award","Turing Award"]}
}}, 0]
},
award2 : { $filter : {
input : "$awards",
as : "award",
cond : {$lt : ["$$award.year", "$turing_award_year"]}
}
}
,
first_name : "$name.first",
last_name : "$name.last"
}}
]
).pretty();