子文档中行的 Mongoose 聚合“$sum”
Posted
技术标签:
【中文标题】子文档中行的 Mongoose 聚合“$sum”【英文标题】:Mongoose aggregation "$sum" of rows in sub document 【发布时间】:2015-10-04 21:14:00 【问题描述】:我对 sql 查询相当擅长,但我似乎无法理解分组和获取 mongo db 文档的总和,
考虑到this,我有一个工作模型,其架构如下:
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);
);
谁能指引我正确的方向。提前谢谢你
【问题讨论】:
聚合有点像 *nix 系统中的管道。将每个运算符视为管道中的一个“阶段”,它转换/减少输入并将新输出发送到下一个阶段。 【参考方案1】:入门
正如前面正确指出的,将聚合“管道”视为 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
“两次”将这些转换为它们自己文档中的普通对象,适合聚合。
所以这些是你需要知道的。正如我之前所说,要“了解”数据是如何“去规范化”的,以及这对您的最终总数意味着什么。
【讨论】:
【参考方案2】:听起来您想在 SQL 等效项中执行 "sum (prices) WHERE owner IS NOT NULL"
。
在这种假设下,您需要先进行 $match,以将输入集减少到您的总和。所以你的第一阶段应该是这样的
$match: all_service.all_sub_items.owner : $exists: true
将此视为将所有匹配的文档传递到您的第二阶段。
现在,因为您正在对数组求和,所以您必须执行另一个步骤。聚合运算符适用于 documents - 没有真正的方法来对数组求和。因此,我们希望扩展您的数组,以便将数组中的每个元素拉出,以在其自己的文档中将数组字段表示为一个值。将此视为交叉连接。这将是$unwind。
$unwind: "$all_service.all_sub_items"
现在您已经制作了更多的文档,但是我们可以对它们进行汇总。现在我们可以执行 $group 了。在您的 $group 中,您指定一个转换。行:
_id: , // not sure what to put here
正在输出文档中创建一个字段,该字段与输入文档不同。因此,您可以在此处随意设置 _id,但可以将其视为等同于 sql 中的“GROUP BY”。 $sum 运算符本质上将为您在此处创建的与该 _id 匹配的每组文档创建一个总和 - 所以本质上我们将通过使用 $group 来“重新折叠”您刚刚对 $unwind 所做的事情。但这将允许 $sum 工作。
我认为您正在寻找仅对您的主文档 ID 进行分组,所以我认为您的问题中的 $sum 语句是正确的。
$group : _id : $_id, totalAmount : $sum : '$all_service.all_sub_item.price'
这将输出具有与您的原始文档 ID 和您的总和等效的 _id 字段的文档。
我会让你把它放在一起,我对节点不是很熟悉。你很接近,但我认为将你的 $match 移到前面并使用 $unwind 阶段会让你到达你需要的地方。祝你好运!
【讨论】:
发现另一个问题有一些 $unwind 例子,你可以看看:***.com/questions/12162681/…以上是关于子文档中行的 Mongoose 聚合“$sum”的主要内容,如果未能解决你的问题,请参考以下文章
使用聚合 mongodb mongoose 将集合子子文档与其他集合子文档连接起来
mongoDB,带有 $sum 和 $count 的 mongoose 聚合查询