【原文地址】https://docs.mongodb.com/manual/mongodb
聚合shell
聚合操做處理數據記錄並返回計算後的結果。聚合操做將多個文檔分組,並能對已分組的數據執行一系列操做而返回單一結果。MongoDB提供了三種執行聚合的方式:聚合管道,map-reduce方法和單一目的聚合操做。數據庫
聚合管道json
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
]
}
aggregate()方法
aggregate() 方法使用聚合管道處理文檔,輸出聚合結果。一個聚合管道由多個階段組成,當文檔通過彙集管道各個階段時,管道處理進入其中的文檔。
在mongo shell中,aggregate() 方法提供了對aggregate 的包裝。
返回人口數量在一千萬以上的州
下面的聚合操做返回全部人口數在一千萬以上的州:
db.zipcodes.aggregate( [
{ $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
{ $match: { totalPop: { $gte: 10*1000*1000 } } }] )
在這個例子中,聚合管道包含 $group階段,其後跟$match階段。
新的關於每一個州的信息的文檔包含兩個字段:_id 字段和totalPop字段。_id字段值是州的名稱,totalPop字段值是經計算後得到的各州的總人口數。爲了計算這個值$group階段使用$sum操做符統計每一個州的人口數。
{
"_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階段。
上面那個階段完成後,管道中的文檔樣式爲:
{
"_id" : {
"state" : "CO",
"city" : "EDGEWATER"
},
"pop" : 13154
}
這個聚合操做返回文檔相似於:
{
"_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階段。
{
"_id" : {
"state" : "CO",
"city" : "EDGEWATER"
},
"pop" : 13154
}
這個階段爲每一個州計算以下四個字段值:使用$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集合中的全部文檔都通過了管道,在管道中執行如下操做:
聚合操做返回結果爲:
{
"name" : "JANE"},{
"name" : "JILL"},{
"name" : "JOE"
}
返回根據加入時間排序後的用戶名稱
下面的聚合操做返回根據加入月份排序的用戶名稱,這種聚合操做有助於生成會員更新提醒。
db.users.aggregate(
[
{ $project :
{
month_joined : { $month : "$joined" },
name : "$_id",
_id : 0
}
},
{ $sort : { month_joined : 1 } }
]
)
Users集合中的全部文檔都通過了管道,在管道中執行如下操做:
操做返回的結果爲:
{
"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 集合中全部文檔都通過管道,在管道中執行以下操做:
month_joined值,$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 集合中全部文檔都通過管道,在管道中執行以下操做:
例如:
下面的文檔:
{
_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"
}
{
"_id" : "golf",
"number" : 33},
{
"_id" : "racquetball",
"number" : 31},
{
"_id" : "swimming",
"number" : 24},
{
"_id" : "handball",
"number" : 19},
{
"_id" : "tennis",
"number" : 18}
}
-----------------------------------------------------------------------------------------
轉載與引用請註明出處。
時間倉促,水平有限,若有不當之處,歡迎指正。