入门
正如前面正确指出的,将聚合“管道”视为 Unix 和其他系统 shell 中的“管道”| 运算符确实会有所帮助。一个“阶段”将输入提供给“下一个”阶段,依此类推。
您需要注意的是,您有“嵌套”数组,一个数组位于另一个数组中,如果您不小心,这可能会对您的预期结果产生巨大影响。
您的文档由顶层的“all_service”数组组成。大概这里经常有“多个”条目,所有条目都包含您的“价格”属性以及“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”中的“管道”第一个数组就像这样:
{ "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”。但这只是获得结果的一部分,因为您应该知道在查询文档以查找数组中的匹配值时会发生什么。
当然,仍然返回“整个”文档,因为这是您真正要求的。数据已经“加入”,我们没有要求以任何方式“取消加入”它。您可以将其视为“第一个”文档选择,但是当“去规范化”时,每个数组元素现在实际上代表了一个“文档”本身。
因此,您不仅在“管道”的开头“仅”$match,在处理“所有”$unwind 语句之后,您还 $match,直到您希望匹配的元素级别.
Job.aggregate(
[
// Match to filter possible "documents"
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
// De-normalize arrays
{ "$unwind": "$all_service" },
{ "$unwind": "$all_service.all_subitem" },
// Match again to filter the array elements
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
// Group on the "_id" for the "key" you want, or "null" for all
{ "$group": {
"_id": null,
"total": { "$sum": "$all_service.all_sub_item.price" }
}}
],
function(err,results) {
}
)
另外,自 2.6 以来的现代 MongoDB 版本也支持 $redact 运算符。在这种情况下,这可以用于在使用$unwind 处理之前“预过滤”数组内容:
Job.aggregate(
[
// Match to filter possible "documents"
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
// Filter arrays for matches in document
{ "$redact": {
"$cond": {
"if": {
"$eq": [
{ "$ifNull": [ "$owner", given_user_id ] },
given_user_id
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}},
// De-normalize arrays
{ "$unwind": "$all_service" },
{ "$unwind": "$all_service.all_subitem" },
// Group on the "_id" for the "key" you want, or "null" for all
{ "$group": {
"_id": null,
"total": { "$sum": "$all_service.all_sub_item.price" }
}}
],
function(err,results) {
}
)
这可以“递归地”遍历文档并测试条件,甚至在$unwind 之前有效地删除任何“不匹配”的数组元素。这可以加快速度,因为不匹配的项目不需要“解开”。但是有一个“问题”,如果由于某种原因“所有者”根本不存在于数组元素上,那么这里所需的逻辑会将其视为另一个“匹配”。您可以随时再次$match 确定,但仍有更有效的方法可以做到这一点:
Job.aggregate(
[
// Match to filter possible "documents"
{ "$match": {
"all_service.all_sub_item.owner": given_user_id
}},
// Filter arrays for matches in document
{ "$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
]
}
}},
[[]]
]
}
}},
// De-normalize the "two" level array. "Double" $unwind
{ "$unwind": "$all_items" },
{ "$unwind": "$all_items" },
// Group on the "_id" for the "key" you want, or "null" for all
{ "$group": {
"_id": null,
"total": { "$sum": "$all_items.price" }
}}
],
function(err,results) {
}
)
与$redact 相比,该过程“大幅”减少了两个数组中项目的大小。 $map 运算符将数组的每个元素处理为“in”中的给定语句。在这种情况下,每个“外部”数组元素都被发送到另一个 $map 以处理“内部”元素。
这里使用$cond 执行逻辑测试,如果满足“条件”,则返回“内部”数组元素,否则返回false 值。
$setDifference 用于过滤返回的任何false 值。或者在“外部”情况下,任何由所有false 值产生的“空白”数组都从没有匹配的“内部”中过滤出来。这仅留下匹配的项目,封装在“双”数组中,例如:
[[{ "_id": 1, "price": 1, "owner": "b" },{..}],[{..},{..}]]
由于“所有”数组元素默认有一个 _id 和猫鼬(这是你保留它的一个很好的理由),所以每个项目都是“不同的”并且不受“设置”运算符的影响,除了删除不匹配的值。
处理$unwind“两次”将这些转换为它们自己文档中的普通对象,适合聚合。
所以这些是你需要知道的。正如我之前所说,要“了解”数据是如何“去规范化”的,以及这对您的最终总数意味着什么。