[英]Mongoose aggregation “$sum” of rows in sub document
我對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);
}
);
有人可以引導我朝着正確的方向前進。 先感謝您
正如前面正確提到的,它確實有助於將聚合“管道”視為“管道” |
來自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 } ] } }
這使得“$ bd”引用的第二個數組進一步被去標准化為最終的非標准化結果“沒有任何數組”。 這允許其他操作進行處理。
對於幾乎“每個”聚合管道,您要做的“第一”事情是“過濾”文檔,只包含那些包含結果的文檔。 這是一個好主意,特別是在執行諸如$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
執行邏輯測試,如果滿足“condiition”,則返回“inner”數組元素,否則返回false
值。
$setDifference
用於過濾掉返回的任何false
值。 或者在“外部”情況下,由所有false
值導致的任何“空白”數組從“內部”過濾而在那里沒有匹配。 這只留下匹配的項目,包含在“雙”數組中,例如:
[[{ "_id": 1, "price": 1, "owner": "b" },{..}],[{..},{..}]]
因為“所有”數組元素默認使用帶有mongoose的_id
(這是你保留它的一個很好的理由)然后每個項目都是“不同的”並且不受“set”運算符的影響,除了刪除不匹配的值。
處理$unwind
“兩次”將它們轉換為自己文檔中的普通對象,適合聚合。
所以這些是你需要知道的事情。 正如我之前所說的那樣,要“了解”數據如何“去標准化”以及這意味着對你的最終總數。
這聽起來像你想要的,在SQL等價物中,做"sum (prices) WHERE owner IS NOT NULL"
。
根據這個假設,您將首先要進行$ match,以減少輸入設置為總和。 所以你的第一階段應該是這樣的
$match: { all_service.all_sub_items.owner : { $exists: true } }
想一想,然后將所有匹配的文檔傳遞到第二階段。
現在,因為你要對一個數組求和,你必須再做一步。 聚合運算符處理文檔 - 實際上沒有一種方法可以對數組求和。 因此,我們希望擴展您的數組,以便在其自己的文檔中拉出數組中的每個元素以將數組字段表示為值。 將此視為交叉連接。 這將是$放松 。
$unwind: { "$all_service.all_sub_items" }
現在你已經制作了更多的文檔,但是我們可以將它們相加。 現在我們可以執行$ group了。 在$ group中,指定轉換。 這條線:
_id: {}, // not sure what to put here
在輸出文檔中創建一個字段,該字段與輸入文檔不同。 所以你可以在這里做任何你想要的東西,但是把它當作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階段將獲得你需要的位置。 祝好運!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.