基础知识
正如之前正确提到的那样,将聚合“管道”视为Unix和其他系统shell中的“pipe”|
运算符会有所帮助。一个“阶段”将输入传递给“下一个”阶段,以此类推。
你需要注意的是,你有“嵌套”的数组,一个数组包含另一个数组,如果不小心处理,这可能会对期望得到的结果产生重大影响。
你的文档由顶层的“all_service”数组组成。假设通常会有“多个”条目在此处,都包含你的“price”属性以及“all_sub_item”。当然,“all_sub_item”本身也是一个数组,也包含许多自己的项目。
你可以将这些数组视为SQL中你的表格之间的“关系”,在每种情况下都是“一对多”的关系。但是数据以“预连接”形式存在,您可以一次获取所有数据而不执行连接操作。你应该已经熟悉了这一点。
但是,当您要在文档之间进行“聚合”时,您需要以与SQL中相同的方式“去规范化”数据,通过“定义”“连接”来将其“去规范化”。这就是将数据转换为适合聚合的非规范化状态的过程。
因此,同样的可视化应用。主文档的条目将由子文档的数量复制,并且与“内部子项”进行“连接”将相应地复制主要和初始“子项”。简而言之,这个:
{
"a": 1,
"b": [
{
"c": 1,
"d": [
{ "e": 1 }, { "e": 2 }
]
},
{
"c": 2,
"d": [
{ "e": 1 }, { "e": 2 }
]
}
]
}
变成这样:
{ "a" : 1, "b" : { "c" : 1, "d" : { "e" : 1 } } }
{ "a" : 1, "b" : { "c" : 1, "d" : { "e" : 2 } } }
{ "a" : 1, "b" : { "c" : 2, "d" : { "e" : 1 } } }
{ "a" : 1, "b" : { "c" : 2, "d" : { "e" : 2 } } }
这个操作可以使用 $unwind
,由于有多个数组需要处理,在继续任何处理之前,您需要对两个数组都进行$unwind
:
db.collection.aggregate([
{ "$unwind": "$b" },
{ "$unwind": "$b.d" }
])
所以在这里,可以像这样从"$b"中提取出第一个数组"pipe":
{ "a" : 1, "b" : { "c" : 1, "d" : [ { "e" : 1 }, { "e" : 2 } ] } }
{ "a" : 1, "b" : { "c" : 2, "d" : [ { "e" : 1 }, { "e" : 2 } ] } }
将"$b.d"引用的第二个数组进一步去规范化,以便最终得到“没有任何数组”的规范化结果。这样可以让其他操作进行处理。
解决方法
使用几乎“每个”汇聚管道时,你想做的“第一件”事情就是过滤掉那些只包含你所需结果的文档。这是个好主意,尤其是在执行 $unwind
等操作时,你不希望对不符合目标数据的文档执行操作。
所以,你需要在数组深度上匹配你的“user_id”。但这只是获得结果的一部分,因为你应该了解当你查询数组中的匹配值时会发生什么。
当然,“整个”文档仍然会被返回,因为这才是你真正请求的内容。数据已经“连接”起来了,我们没有以任何方式“取消连接”。你可以像“首先”选择文档一样看待这一点,但是当“去规范化”时,每个数组元素现在实际上代表着一个“文档”本身。
所以,你不仅要在“pipeline”开头进行$match
操作,而且在处理完“所有”$unwind
语句后,还要进行$match
操作,一直到你想要匹配的元素的级别。
Job.aggregate(
[
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
{ "$unwind": "$all_service" },
{ "$unwind": "$all_service.all_subitem" },
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
{ "$group": {
"_id": null,
"total": { "$sum": "$all_service.all_sub_item.price" }
}}
],
function(err,results) {
}
)
从2.6版本开始,现代的MongoDB版本还支持$redact
操作符。在处理$unwind
之前,可以使用它来“预过滤”数组内容:
Job.aggregate(
[
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
{ "$redact": {
"$cond": {
"if": {
"$eq": [
{ "$ifNull": [ "$owner", given_user_id ] },
given_user_id
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}},
{ "$unwind": "$all_service" },
{ "$unwind": "$all_service.all_subitem" },
{ "$group": {
"_id": null,
"total": { "$sum": "$all_service.all_sub_item.price" }
}}
],
function(err,results) {
}
)
可以“递归地”遍历文档并测试条件,有效地在您甚至执行$unwind
之前删除任何“不匹配”的数组元素。这样可以加快速度,因为不匹配的项不需要被“展开”。但是,如果由于某些原因数组元素上根本不存在“所有者”,则此处所需的逻辑将将其视为另一个“匹配”。您始终可以再次$match
以确保,但仍有更有效的方法:
Job.aggregate(
[
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
{ "$project": {
"all_items": {
"$setDifference": [
{ "$map": {
"input": "$all_service",
"as": "A",
"in": {
"$setDifference": [
{ "$map": {
"input": "$$A.all_sub_item",
"as": "B",
"in": {
"$cond": {
"if": { "$eq": [ "$$B.owner", given_user_id ] },
"then": "$$B",
"else": false
}
}
}},
false
]
}
}},
[[]]
]
}
}},
{ "$unwind": "$all_items" },
{ "$unwind": "$all_items" },
{ "$group": {
"_id": null,
"total": { "$sum": "$all_items.price" }
}}
],
function(err,results) {
}
)
该过程与
$redact
相比,极大地减少了两个数组中项目的大小。
$map
操作符将每个数组元素发送到另一个
$map
以处理“内部”元素。在这种情况下,每个“外部”数组元素都被发送到另一个
$map
中以处理“内部”元素。
使用
$cond
进行逻辑测试,如果满足“条件”,则返回“内部”数组元素,否则返回
false
值。
使用
$setDifference
来过滤掉任何返回的
false
值。或者在“外部”情况下,从所有从“内部”中过滤掉所有
false
值得到的“空白”数组中过滤掉任何“空白”数组,这里没有匹配。这只留下匹配项,包含在“双重”数组中,例如:
[[{ "_id": 1, "price": 1, "owner": "b" },{..}],[{..},{..}]]
由于mongoose默认情况下“所有”数组元素都有一个_id(这也是保留它的一个很好的理由),因此每个项都是“不同的”,并且不受“set”运算符的影响,除了删除不匹配的值。
使用$unwind
“两次”将其转换为自己的文档中的普通对象,适合聚合。
所以这些就是你需要知道的事情。如我之前所述,请“注意”数据“去规范化”的方式以及这对最终总数的影响。