子文档中的行的Mongoose聚合“$ sum”

我对sql查询相当不错,但似乎无法绕过分组并获取mongo db文档的总和,

考虑到这一点,我有一个模式如下的工作模式:

{ name: { type: String, required: true }, info: String, active: { type: Boolean, default: true }, all_service: [ price: { type: Number, min: 0, required: true }, all_sub_item: [{ name: String, price:{ // << -- this is the price I want to calculate type: Number, min: 0 }, owner: { user_id: { // <<-- here is the filter I want to put type: Schema.Types.ObjectId, required: true }, name: String, ... } }] ], date_create: { type: Date, default : Date.now }, date_update: { type: Date, default : Date.now } } 

我想有一个price栏的总和, owner在场,我在下面尝试,但没有运气

  Job.aggregate( [ { $group: { _id: {}, // not sure what to put here amount: { $sum: '$all_service.all_sub_item.price' } }, $match: {'not sure how to limit the user': given_user_id} } ], //{ $project: { _id: 1, expense: 1 }}, // you can only project fields from 'group' function(err, summary) { console.log(err); console.log(summary); } ); 

有人能指导我正确的方向吗? 先谢谢你

底漆


正如前面正确地指出的那样,将“pipe道”与“pipe道”一样考虑到聚合“pipe道”是有帮助的 来自Unix和其他系统shell的运算符。 一个“阶段”为“下一个”阶段提供input,等等。

这里需要注意的是,你有“嵌套的”数组,一个数组在另一个数组中,如果你不小心,这可能会对你的预期结果产生巨大的差异。

您的文档由顶层的“all_service”数组组成。 据推测,这里通常有“多个”条目,都包含“价格”属性以及“all_sub_item”。 那么当然“all_sub_item”本身就是一个数组,也包含着它自己的许多项目。

您可以将这些数组视为SQL中的表之间的“关系”,每种情况下都是“一对多”。 但数据处于“预join”的forms,您可以一次获取所有数据而无需执行连接。 你应该已经很熟悉了。

然而,当你想要“聚合”文档时,你需要通过“定义”“连接”来以“去规范化”这种方式,就像在SQL中一样。 这是为了将数据“转换”为适合聚合的非归一化状态。

所以相同的可视化适用。 主文档的条目被复制的子文档的数量,“join”到“内部孩子”将相应地复制主和初始“孩子”。 简而言之,这个:

 { "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": "$bd" } ]) 

所以那里的“pipe”第一个arrays从“$ b”像这样:

 { "a" : 1, "b" : { "c" : 1, "d" : [ { "e" : 1 }, { "e" : 2 } ] } } { "a" : 1, "b" : { "c" : 2, "d" : [ { "e" : 1 }, { "e" : 2 } ] } } 

剩下的由“$ bd”引用的第二个数组进一步被去归一化为最终的非归一化结果“没有任何数组”。 这允许其他操作进行处理。

解决


几乎所有的聚合pipe道,你要做的“第一件事”是将文档“过滤”到只包含结果的文档。 这是一个好主意,尤其是在执行诸如$unwind操作时,那么您不希望在那些与您的目标数据不匹配的文档上这么做。

所以你需要在数组深度匹配你的“user_id”。 但这只是获得结果的一部分,因为您应该知道在查询文档中数组中匹配的值时会发生什么情况。

当然,这个“整个”文档还是会返回的,因为这是你真正要求的。 数据已经“join”了,我们还没有要求以任何方式“解除连接”。您将这看作是“第一个”文档select,但是当“解规范化”时,每个数组元素现在实际上代表了一个“文件”本身。

因此,在“pipe道”开始处不要“只” $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) { } ) 

这可以“recursion地”遍历文档并testing条件,在甚至$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 ”相比,该过程将“彻底”减less了两个arrays中项目的大小。 $map操作符将数组中的每个元素处理为“in”中的给定语句。 在这种情况下,每个“外部”数组元素被发送到另一个$map来处理“内部”元素。

在这里用$cond来执行逻辑testing,如果符合“condiition”则返回“inner”数组元素,否则返回false值。

$setDifference用于过滤所有返回的false值。 或者在“外部”情况下,所有false值所产生的任何“空白”数组都会从“内部”过滤掉,在这里没有匹配。 这只留下匹配的项目,装在一个“双”数组中,例如:

 [[{ "_id": 1, "price": 1, "owner": "b" },{..}],[{..},{..}]] 

由于“所有”数组元素在默认情况下有一个_id ,所以每个元素都是“distinct”,不受“set”操作符的影响,除了删除不匹配的值。

处理$unwind “两次”,将它们转换为适合聚合的自己文档中的普通对象。

所以这些是你需要知道的事情。 正如我刚才所说的那样,要“意识到”数据如何“不规范化”,以及这对于您的最终总计而言意味着什么。

这听起来像你想,在SQL等价,做"sum (prices) WHERE owner IS NOT NULL"

在这个假设下,你会想先做你的$匹配,以减lessinput设置为你的总和。 所以你的第一个阶段应该是这样的

$match: { all_service.all_sub_items.owner : { $exists: true } }

想想这个,然后把所有的配套文件都传到第二阶段。

现在,因为你正在总结一个数组,所以你必须做另一个步骤。 汇总操作符处理文档 – 实际上没有办法对数组求和。 所以我们想要扩展你的数组,使得数组中的每个元素都被拉出来在它自己的文档中将数组字段表示为一个值。 把这看成是一个交叉连接。 这将是$放松 。

$unwind: { "$all_service.all_sub_items" }

现在你只是创造了更多的文件,但是我们可以把它们总结出来。 现在我们可以执行$组。 在你的$组中,你指定了一个转换。 该行:

_id: {}, // not sure what to put here

输出文档中创build一个字段,这与input文档不是相同的文档。 所以你可以在这里使用_id,但是在sql中把它看作等价于你的“GROUP BY”。 $ sum操作符将基本上为您在这里创build的每个文档组创build一个与_id匹配的总和,所以基本上我们将使用$ group“重新折叠”您使用$ unwind所做的操作。 但是,这将允许$总和工作。

我认为你正在寻找只是你的主要文件编号的分组,所以我认为你的问题中的$ sum声明是正确的。

$group : { _id : $_id, totalAmount : { $sum : '$all_service.all_sub_item.price' } }

这将输出一个_id字段相当于您的原始文件ID和您的总和文件。

我会让你把它放在一起,我不是非常熟悉节点。 你很近,但我认为将你的$匹配放在前面,使用$ unwind阶段将会使你得到你需要的位置。 祝你好运!