【翻譯】MongoDB指南/聚合——聚合管道
【原文地址】https://docs.mongodb.com/manual/
聚合
聚合操作處理資料記錄並返回計算後的結果。聚合操作將多個文件分組,並能對已分組的資料執行一系列操作而返回單一結果。MongoDB提供了三種執行聚合的方式:聚合管道,map-reduce方法和單一目的聚合操作。
聚合管道
MongoDB的聚合框架模型建立在資料處理管道這一概念的基礎之上。文件進入多階段管道中,管道將文件轉換為聚合結果。最基本的管道階段類似於查詢過濾器和修改輸出文件形式的文件轉換器。
其他的管道為分組和排序提供一些工具,可通過指定一個或多個欄位完成分組或排序;同時提供了聚合陣列內容的工具,操作的陣列包括文件陣列。另外,聚合階段能夠使用一些運算子,完成諸如計算均值或連線字串之類的任務。
管道利用MongoDB本機的操作方法提供了有效的資料聚合操作,並且對於資料聚合來說採用本機的操作方法是首選的。
聚合管道支援在分片集合上執行操作。
聚合管道在它的某些階段能夠使用索引來提高效能。另外,聚合管道有一個內部優化階段。
Map-Reduce
MongoDB也能夠提供map-reduce操作來完成聚合。一般地,map-reduce操作有兩個階段:map 階段處理每一個文件並將每一個輸入文件對映成一個或多個物件,reduce合成map階段的輸出。可選的,map-reduce操作可以有一個finalize階段以對輸出做最後的更改。像其他的聚集操作一樣,
map-reduce操作能夠指定查詢條件篩選輸入文件和對結果進行排序和限制。
map-reduce使用自定義JavaScript方法來實現map,reduce和finalize 操作。雖然與聚合管道相比,自定義JavaScript提供了極大的靈活性,
但map-reduce比聚合管道效率低且比聚合管道更復雜。
map-reduce可以在分片集合上執行操作。map-reduce操作也能將資料輸出到分片集合上。
注:
從2.4版本開始,某些mongo shell 方法和特性不支援map-reduce操作。2.4版本也支援同時執行多個JavaScript操作。2.4之前的版本,
JavaScript程式碼在單執行緒中執行,對map-reduce操作來說存在併發問題。
單一目的聚合操作
MongoDB還提供了db.collection.count(), db.collection.group(), db.collection.distinct()專用資料庫命令。
所有這些操作從一個集合中聚合文件。雖然這些操作提供了簡單的實現聚合操作的方式,但是它們缺乏靈活性和同聚合管道與
map-reduce相似的效能。
1 聚合管道
聚合管道是一個建立在資料處理管道模型概念基礎上的框架。文件進入多階段管道中,管道將文件轉換為聚合結果。
聚合管道提供了map-reduce 的替代品,並且對於 map-reduce的複雜性是多餘的聚合任務來說,聚合管道可能是首選的解決方案。
聚合管道對值的型別和返回結果的大小做了限制。
1.1 管道
MongoDB 聚合管道由多個階段組成。當文件經過各個管道時,每個管道對文件進行變換。對於每一個輸入文件,管道各階段不需要產生輸出文件。例如,某些階段可能會生成新文件或過濾掉一些文件。聚合管道的一些階段可以在管道中出現多次。
MongoDB提供了可在mongo shell中執行的db.collection.aggregate()方法和聚合管道命令aggregate。
1.2 聚合管道表示式
某些管道階段採用聚合管道表示式作為它的運算元。聚合管道表示式指定了應用於輸入文件的轉換。聚合管道表示式採用文件結構並且可以包含其他聚合管道表示式。
聚合管道表示式能夠僅作用於管道中的當前文件並且不會涉及其他文件資料:聚合管道表示式支援在記憶體中執行文件轉換。
一般地,聚合管道表示式是無狀態的並且僅在被聚合處理過程發現時才被求值,但累加器表示式除外。
累加器用在$group階段,當文件經過這個管道時,它們的狀態被儲存下來(例如總數,最大值,最小值,相關資料)。
3.2版本中的變化:某些累加器在$project階段可以使用。然而,在$project階段使用這些累加器時,這些累加器不會儲存它們的狀態到文件中。
1.3 聚合管道行為
在MongoDB中聚合命令作用於一個集合,在邏輯上將整個集合傳入聚合管道。為了優化操作,儘可能地使用下面的策略以避免掃描整個集合。
管道操作符合索引
$match 和$sort管道操作符能夠利用索引,當它們在管道開始處出現時。
2.4版本的變化:$geoNear管道操作符能夠利用地理空間索引。當使用$geoNear時,$geoNear管道操作符必須出現在聚合管道的第一階段。
3.2版本中的變化:從3.2版本開始索引能夠覆蓋一個聚合管道。在2.6 和3.0版本中,索引不能覆蓋聚合管道,因為即使管道使用了索引,聚合還是需要利用實際的文件。
較早地過濾
如果你的聚合操作僅需要集合中的一個數據子集,那麼使用$match, $limit,和$skip階段來限制最開始進入管道的文件。當被放到管道的開始處時,$match操作使用合適的索引,只掃描集合中匹配到的文件。
在管道的開始處使用後面緊跟了$sort階段的$match管道階段,這在邏輯上等價於使用了索引的帶有排序的查詢操作。儘可能地將$match階段放在管道的最開始處。
其他的特性
聚合管道有一個內部最優化階段,這個階段改進了某些操作的效能。
聚合管道支援分片集合上的操作。
1.4 聚合管道優化
聚合管道操作有一個優化階段,此階段試圖重塑管道以改進效能。
為檢視優化程式如何改進一個特定的聚合管道,在db.collection.aggregate()方法中使用explain 選項。
1.4.1 投影器優化
聚合管道能夠判定是否使用集合中欄位的一個子集來獲得結果。如果使用子集,那麼聚合管道將只會使用那些需要的欄位以減少管道中傳輸的資料量。
1.4.2 管道順序優化
$sort + $match管道順序優化
當管道順序為$sort 後跟$match時, $match會移動到$sort之前以減少排序物件的數量。例如,如果管道包含下面的階段:
{ $sort: { age : -1 } },{ $match: { status: 'A' } }
在優化階段,優化器將佇列順序改變為下面這樣:
{ $match: { status: 'A' } },{ $sort: { age : -1 } }
$skip + $limit管道順序優化
當管道順序為$skip 後跟$limit時, $limit會移動到$skip 之前以減少排序物件的數量。順序改變後,$limit值增加的值為$skip的值。
例如,如果管道包含下面的階段:
{ $skip: 10 },{ $limit: 5 }
在優化階段,優化器將佇列順序改變為下面這樣:
{ $limit: 15 },{ $skip: 10 }
這種優化為$sort + $limit合併提供更多的機會,例如序列$sort + $skip + $limit。
對於分片集合上的聚合操作,這種優化減少了每一個分片返回的結果。
$redact + $match管道順序優化
當管道包含了之後緊跟$match階段的$redact階段時,儘可能地,管道會不時地在 $redact階段前新增一部分$match階段。如果新增的$match階段是管道的開始,管道會在查詢的同時使用索引來限制進入管道的文件數量。
例如,如果管道包含下面的階段:
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }
優化程式能夠在$redact階段之前新增相同的$match階段:
{ $match: { year: 2014 } },
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }
$project + $skip 或$limit管道順序優化
3.2版本新增
當管道順序為$projec後跟$skip或$limit時,$skip或$limit會移動到$projec之前,
例如,如果管道包含下面的階段:
{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $limit: 5 }
在優化階段,優化器將佇列順序改變為下面這樣:
{ $sort: { age : -1 } },
{ $limit: 5 },
{ $project: { status: 1, name: 1 } }
這種優化為$sort + $limit合併提供更多的機會,例如序列$sort + $limit。
1.4.3 管道合併優化
這個優化階段將一個管道階段與它之前的管道階段合併。一般地,合併發生在階段重新排序之後。
合併$sort + $limit
當$sort後面緊跟$limit時,優化程式能將$limit合併到$sort,這使得排序操作僅儲存結果集中的前n條資料並處理它,n是指定的限制,MongoDB只需要在記憶體中儲存n個條目。
當設定allowDiskUse 為true時並且n條資料已經超過了聚合記憶體的限制,上面這種優化仍然會被採用。
合併$limit + $limit
當 $limit後面緊跟另一個$limit時,兩個階段合併為一個階段,合併後的限制值為兩者中最小值。
例如,如果管道包含下面的階段:
{ $limit: 100 },
{ $limit: 10 }
第二個$limit階段被合併到第一個$limit階段中,合併後的限制值為100和10中最小的,即10。
{ $limit: 10 }
合併$skip + $skip
當 $skip後面緊跟另一個$skip時,兩個$skip合併為一個$skip,跳過的數量為兩者之和。
例如,如果管道包含下面的階段:
{ $skip: 5 },
{ $skip: 2 }
第二個$skip被合併到第一個$skip中,合併後跳過的數量為5和2之和。
{ $skip: 7 }
合併$match + $match
當 $match後面緊跟另一個$match時,兩個階段合併為一個結合使用$and的$match,跳過的數量為兩者之和。
例如,如果管道包含下面的階段:
{ $match: { year: 2014 } },
{ $match: { status: "A" } }
第二個$match被合併到第一個$match中。
{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }
合併$lookup + $unwind
3.2版本新增
當$lookup之後緊跟$unwind並且$unwind 操作$lookup的欄位,優化階段能夠將$unwind合併到$lookup中。這避免了建立較大的中間文件。
例如,如果管道包含下面的階段:
{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y"
}
},
{ $unwind: "$resultingArray"}
優化器將$unwind合併到$lookup中。如果執行聚合的時候使用explain 選項,輸出的合併階段為:
{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y",
unwinding: { preserveNullAndEmptyArrays: false }
}
}
1.5例子
下面例子所示的一些序列能夠利用重新排序和合並優化。一般地,合併發生在重新排序之後。
序列$sort + $skip + $limit
管道包含$sort階段,其後接$skip階段,$skip階段後接 $limit階段
{ $sort: { age : -1 } },{ $skip: 10 },{ $limit: 5 }
首先,優化程式將$skip + $limit轉化為下面的順序:
{ $sort: { age : -1 } },
{ $limit: 15 },
{ $skip: 10 }
目前的序列為$sort階段後跟$limit階段,管道能夠合併這兩個過程以減少排序階段對記憶體的消耗。
序列$limit + $skip + $limit + $skip
一個管道包含了$limit和$skip交替出現的序列:
{ $limit: 100 },
{ $skip: 5 },
{ $limit: 10 },
{ $skip: 2 }
優化程式將{ $skip: 5 } 和{ $limit: 10 } 順序反轉,並增大限制數量:
{ $limit: 100 },
{ $limit: 15},
{ $skip: 5 },
{ $skip: 2 }
優化程式能夠將兩個$limit合併,將兩個$skip合併,結果為:
{ $limit: 15 },
{ $skip: 7 }
1.6 聚合管道限制
使用聚合命令有如下限制:
結果大小限制
2.6版本中變化
從2.6版本開始,聚合命令(aggregate)能夠返回一個遊標或將結果儲存在集合中。當返回遊標或者將結果儲存到集合中時,結果集中的每一個文件受限於BSON文件大小,目前BSON文件大小最大允許為16MB;如果任何一個文件的大小超過了這個值,聚合命令將丟擲一個錯誤。這個限制只作用於返回的文件,在管道中被處理的文件有可能超出這個閾值。從2.6開始,db.collection.aggregate() 方法預設返回遊標。
如果不指定遊標選項或者將結果儲存到集合中,aggregate 命令返回一個BSON文件,文件有一個包含結果集的欄位。文件的大小超過了BSON文件允許的最大值,聚合命令將丟擲一個錯誤。
在更早的版本中,aggregate僅能返回一個包含結果集的BSON文件,如果文件的大小超過了BSON文件允許的最大值,聚合命令將丟擲一個錯誤。
記憶體限制
2.6版本中變化
管道階段對記憶體的限制為100MB。如果某一階段使用的記憶體超過100MB,MongoDB 會丟擲一個錯誤。為了能夠處理大資料集,
使用allowDiskUse選項使聚合管道階段將資料寫入臨時檔案。
1.7聚合管道和分片集合
聚合管道支援分片集合上的操作。
行為
3.2版本中的變化
如果聚合管道以$match開始,精確地匹配一個片鍵,整個聚合管道僅執行在匹配到的分片上。之前的版本中,管道會被拆分,合併的工作要在主分片上完成。
對於要執行在多個分片上的聚合操作,如果操作不需要執行在資料庫的主分片上,這些操作將會路由結果到任意分片來合併結果以避免資料庫主分片過載。
$out階段和$lookup階段需要執行在資料庫主分片上。
優化
當把聚和管道分成兩個部分時,在考慮優化的情況下,拆分管道時確保每一個分片執行階段數量儘可能多。
要檢視管道如何被拆分,使用db.collection.aggregate()和explain選項。
1.8 郵政編碼資料集上的聚合操作
示例中使用集合zipcodes ,這個集合可以從:http://media.mongodb.org/zips.json處獲得。使用mongoimport將資料匯入你的mongod 例項。
資料模型
集合zipcodes中的每一文件的樣式如下:
{
"_id": "10280",
"city": "NEW YORK",
"state": "NY",
"pop": 5574,
"loc": [
-74.016323,
40.710537
]
}
- _id欄位值為字串形式的郵政編碼。
- city 欄位值為城市名稱。一個城市可有多個郵政編碼,城市的不同城區郵政編碼不同。
- State欄位值為兩個字母的州名稱縮寫。
- pop欄位值為人口數量。
- Loc欄位值為用經緯度表示的方位。
aggregate()方法
aggregate() 方法使用聚合管道處理文件,輸出聚合結果。一個聚合管道由多個階段組成,當文件經過聚集管道各個階段時,管道處理進入其中的文件。
在mongo shell中,aggregate() 方法提供了對aggregate 的包裝。
返回人口數量在一千萬以上的州
下面的聚合操作返回所有人口數在一千萬以上的州:
db.zipcodes.aggregate( [
{ $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
{ $match: { totalPop: { $gte: 10*1000*1000 } } }] )
在這個例子中,聚合管道包含 $group階段,其後跟$match階段。
- $group階段根據state 欄位將zipcode 集合分組,計算每一個州的totalPop欄位值,輸出結果為每個州對應一個文件。
新的關於每個州的資訊的文件包含兩個欄位:_id 欄位和totalPop欄位。_id欄位值是州的名稱,totalPop欄位值是經計算後獲得的各州的總人口數。為了計算這個值$group階段使用$sum操作符統計每個州的人口數。
- 經過$group管道階段後的在管道中的文件樣式如下:
{
"_id" : "AK",
"totalPop" : 550043
}
$match階段過濾分組後的文件,僅輸出那些totalPop值大於等於一千萬的文件。$match階段不會修改文件而是輸出未修改的匹配到的文件。
與聚合操作等價的SQL語句為:
SELECT state, SUM(pop) AS totalPop
FROM zipcodes
GROUP BY state
HAVING totalPop >= (10*1000*1000)
返回每個州的城市人口平均值
下面的聚合操作返回每個州的城市人口平均值
db.zipcodes.aggregate( [
{ $group: { _id: { state: "$state", city: "$city" }, pop: { $sum: "$pop" } } },
{ $group: { _id: "$_id.state", avgCityPop: { $avg: "$pop" } } }]
)
在這個例子中,聚合操作包含了兩個$group階段。
- 第一個$group 階段根據city和state欄位組合將文件分組,$sum 表示式根據每個組合計算人口數,並輸出文件,每一個城市和州的組合對應一個文件。
上面那個階段完成後,管道中的文件樣式為:
{
"_id" : {
"state" : "CO",
"city" : "EDGEWATER"
},
"pop" : 13154
}
- 第二個$group階段根據_id.state欄位將文件分組(state欄位在_id文件內),使用$avg表示式計算每一個城市人口的平均值(avgCityPop)並輸出文件,每個州對應一個文件。
這個聚合操作返回文件類似於:
{
"_id" : "MN",
"avgCityPop" : 5335
}
返回州中規模最大和最小的城市
下面的聚合操作返回每個州人口數最多和最少的城市。
db.zipcodes.aggregate( [
{ $group:
{
_id: { state: "$state", city: "$city" },
pop: { $sum: "$pop" }
}
},
{ $sort: { pop: 1 } },
{ $group:
{
_id : "$_id.state",
biggestCity: { $last: "$_id.city" },
biggestPop: { $last: "$pop" },
smallestCity: { $first: "$_id.city" },
smallestPop: { $first: "$pop" }
}
},
// the following $project is optional, and
// modifies the output format.
{ $project:
{ _id: 0,
state: "$_id",
biggestCity: { name: "$biggestCity", pop: "$biggestPop" },
smallestCity: { name: "$smallestCity", pop: "$smallestPop" }
}
}]
)
在這個聚合操作中包含了兩個$group階段,一個$sort階段,一個$project階段。
- 第一個$group 階段根據city和state欄位組合將文件分組,$sum 表示式根據每個組合計算人口數(一個城市可能有多個郵政編碼,因為一個城市的不同區有不同的郵政編碼),並輸出文件,每一個城市和州的組合對應一個文件。這個階段文件類似於:
{
"_id" : {
"state" : "CO",
"city" : "EDGEWATER"
},
"pop" : 13154
}
- $sort階段根據pop欄位的值為管道中的文件排序,順序為從小到大;例如遞增的順序。這個操作不會修改文件。
- 第二個$group 階段根據_id.state欄位對當前已排序的文件分組(例如,state 欄位在_id文件中)並輸出每個州對應的文件。
這個階段為每個州計算如下四個欄位值:使用$last表示式,$group操作符建立biggestCity 和biggestPop欄位,biggestPop欄位值為最大的人口數,biggestCity值為biggestPop對應的城市名稱。使用$first 表示式,$group操作符建立了smallestCity和smallestPop,smallestPop為最小的人口數,smallestCity為smallestPop對應的城市名稱。
管道中這個階段的文件類似於:
{
"_id" : "WA",
"biggestCity" : "SEATTLE",
"biggestPop" : 520096,
"smallestCity" : "BENGE",
"smallestPop" : 2
}
最後的$project階段將_id欄位重新命名為state 並將biggestCity, biggestPop, smallestCity, 和smallestPop移到嵌入式文件biggestCity 和
smallestCity中。
上面這個聚合操作的結果類似於:
{
"state" : "RI",
"biggestCity" : {
"name" : "CRANSTON",
"pop" : 176404
},
"smallestCity" : {
"name" : "CLAYVILLE",
"pop" : 45
}
}
1.9 使用者引用資料的聚合操作
資料模型
假設一個體育俱樂部有一個包含users集合資料庫,users集合中的文件包含使用者的加入日期和喜歡的運動,文件樣式如下:
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : ["golf", "racquetball"]
}
{
_id : "joe",
joined : ISODate("2012-07-02"),
likes : ["tennis", "golf", "swimming"]
}
文件規範化和排序
下面的操作返回的文件中,使用者名稱稱轉成大寫並按字母順序排序。操作如下:
db.users.aggregate(
[
{ $project : { name:{$toUpper:"$_id"} , _id:0 } },
{ $sort : { name : 1 } }
])
Users集合中的所有文件都經過了管道,在管道中執行以下操作:
- $project操作符:
- 建立名為name的欄位。
- 使用$toUpper操作符將_id欄位值轉換成大寫。然後將值儲存在名為name 的欄位中。
- 阻止_id欄位。$project 操作符預設允許_id欄位通過,除非明確地阻止。
- $sort操作符根據name欄位對結果進行排序。
聚合操作返回結果為:
{
"name" : "JANE"},{
"name" : "JILL"},{
"name" : "JOE"
}
返回根據加入時間排序後的使用者名稱稱
下面的聚合操作返回根據加入月份排序的使用者名稱稱,這種聚合操作有助於生成會員更新提醒。
db.users.aggregate(
[
{ $project :
{
month_joined : { $month : "$joined" },
name : "$_id",
_id : 0
}
},
{ $sort : { month_joined : 1 } }
]
)
Users集合中的所有文件都經過了管道,在管道中執行以下操作:
- $project操作符:
- 建立兩個欄位month_joined 和name。
- 阻止結果集中的id輸出。$project 操作符預設允許_id欄位通過,除非明確地阻止。
- $month操作符將joined欄位的值轉換為以整數表示的月份。然後$project操作符將這些值指定給month_joined欄位。
- $sort操作符根據month_joined欄位對結果進行排序。
操作返回的結果為:
{
"month_joined" : 1,
"name" : "ruth"},{
"month_joined" : 1,
"name" : "harold"},{
"month_joined" : 1,
"name" : "kate"}{
"month_joined" : 2,
"name" : "jill"
}
返回每個月加入會員的總數
下面的操作展示了每個月有多少人成為會員。你或許可以利用這些聚合資料來考慮是否招聘新員工和制定營銷策略。
db.users.aggregate(
[
{ $project : { month_joined : { $month : "$joined" } } } ,
{ $group : { _id : {month_joined:"$month_joined"} , number : { $sum : 1 } } },
{ $sort : { "_id.month_joined" : 1 } }
]
)
users 集合中所有文件都經過管道,在管道中執行如下操作:
- $project操作符建立了一個新欄位month_joined。
- $month操作符將joined欄位的值轉換為以整數表示的月份。然後$project操作符將這些值指定給month_joined欄位。
- $group操作符將所有文件按month_joined值分組,並計算每個month_joined欄位值對應多少個文件。特別地,對於每一個唯一的
month_joined值,$group建立了一個新的“每個月”的文件,該文件包含了兩個欄位:
- _id欄位,包含一個嵌入式文件,嵌入式文件有一個month_joined欄位。
- number欄位,這是一個新生成的欄位。對每一個包含給定month_joined欄位值的文件,$sum操作符將number欄位值加1.
- $sort操作符根據month_joine欄位將$group操作符處理過的文件排序。
這個聚和操作的結果為:
{
"_id" : {
"month_joined" : 1
},
"number" : 3},
{
"_id" : {
"month_joined" : 2
},
"number" : 9},
{
"_id" : {
"month_joined" : 3
},
"number" : 5}
返回五種最常見的“愛好”
下面的聚合操作選出五個最常見“愛好”。這種型別的分析有助於發展規劃。
db.users.aggregate(
[
{ $unwind : "$likes" },
{ $group : { _id : "$likes" , number : { $sum : 1 } } },
{ $sort : { number : -1 } },
{ $limit : 5 }
]
)
users 集合中所有文件都經過管道,在管道中執行如下操作:
- $unwind操作符將陣列likes中的每一個元素分離,併為每一個元素建立一個原文件的新版本。
例如:
下面的文件:
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : ["golf", "racquetball"]
}
$unwind操作符建立的文件為:
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : "golf"
}
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : "racquetball"
}
- $group操作符根據likes欄位值分組並計算每組的數量。使用這些資訊,$group建立含有兩個欄位的新文件:
- _id欄位,包含likes欄位值。
- number新生成的欄位,對於包含給定likes欄位值的每個文件$sum操作符將number加1。
- $sort操作符根據number欄位將文件順序反轉。
- $limit 操作符限制結果集中僅包含前五個文件。
{
"_id" : "golf",
"number" : 33},
{
"_id" : "racquetball",
"number" : 31},
{
"_id" : "swimming",
"number" : 24},
{
"_id" : "handball",
"number" : 19},
{
"_id" : "tennis",
"number" : 18}
}
-----------------------------------------------------------------------------------------
時間倉促,水平有限,如有不當之處,歡迎指正。