硬貨來了!輕鬆掌握 MongDB 流式聚合操作
- 2019 年 11 月 20 日
- 筆記
「
閱讀本文大概需要 15 分鐘。
」
資訊科學中的聚合是指對相關數據進行內容篩選、處理和歸類並輸出結果的過程。MongoDB 中的聚合是指同時對多個文檔中的數據進行處理、篩選和歸類並輸出結果的過程。數據在聚合操作的過程中,就像是水流過一節一節的管道一樣,所以 MongoDB 中的聚合又被人稱為流式聚合。
MongoDB 提供了幾種聚合方式:
•Aggregation Pipeline •Map-Reduce•簡單聚合
接下來,我們將全方位地了解 MongoDB 中的聚合。
Aggregation Pipeline
Aggregation Pipeline 又稱聚合管道。開發者可以將多個文檔傳入一個由多個 Stage
組成的 Pipeline
,每一個 Stage
處理的結果將會傳入下一個 Stage
中,最後一個 Stage
的處理結果就是整個 Pipeline
的輸出。
創建聚合管道的語法如下:
db.collection.aggregate( [ { <stage> }, ... ] )
MongoDB 提供了 23 種 Stage
,它們是:
描述
文檔、Stage
和 Pipeline
的關係如下圖所示:

在這裡插入圖片描述
上圖描述了文檔經過 $match
、$sample
和 $project
等三個 Stage
並輸出的過程。SQL 中常見的聚合術語有 WHERE
、SUM
和 COUNT
等。下表描述了常見的 SQL 聚合術語、函數和概念以及對應的 MongoDB 操作符或 Stage
。
MongoDB
下面,我們將通過示例了解 Aggregate
、 Stage
和 Pipeline
之間的關係。
概念淺出
$match
的描述為「過濾文檔,僅允許匹配的文檔地傳遞到下一個管道階段」。其語法格式如下:
{ $match: { <query> } }
在開始學習之前,我們需要準備以下數據:
> db.artic.insertMany([ ... { "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 }, ... { "_id" : 2, "author" : "dave", "score" : 85, "views" : 521 }, ... { "_id" : 3, "author" : "anna", "score" : 60, "views" : 706 }, ... { "_id" : 4, "author" : "line", "score" : 55, "views" : 300 } ... ])
然後我們建立只有一個 Stage
的 Pipeline
,以實現過濾出 author
為 dave
的文檔。對應示例如下:
> db.artic.aggregate([ ... {$match: {author: "dave"}} ... ]) { "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 } { "_id" : 2, "author" : "dave", "score" : 85, "views" : 521 }
如果要建立有兩個 Stage
的 Pipeline
,那麼就在 aggregate
中添加一個 Stage
即可。現在有這樣一個需求:統計集合 artic
中 score
大於 70
且小於 90
的文檔數量。這個需求分為兩步進行:
•過濾出符合要求的文檔•統計文檔數量
Aggregation 非常適合這種多步驟的操作。在這個場景中,我們需要用到 $match
、$group
這兩個 Stage
,然後再與聚合表達式 $sum
相結合,對應示例如下:
> db.artic.aggregate([ ... {$match: {score: {$gt: 70, $lt: 90}}}, ... {$group: {_id: null, number: {$sum: 1}}} ... ]) { "_id" : null, "number" : 2 }
這個示例的完整過程可以用下圖表示:

在這裡插入圖片描述
通過上面的描述和舉例,我相信你對 Aggregate
、 Stage
和 Pipeline
有了一定的了解。接下來,我們將學習常見的 Stage
的語法和用途。
常見的 Stage
sample
$sample
的作用是從輸入中隨機選擇指定數量的文檔,其語法格式如下:
{ $sample: { size: <positive integer> } }
假設要從集合 artic
中隨機選擇兩個文檔,對應示例如下:
> db.artic.aggregate([ ... {$sample: {size: 2}} ... ]) { "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 } { "_id" : 3, "author" : "anna", "score" : 60, "views" : 706 }
size
對應的值必須是正整數,如果輸入負數會得到錯誤提示:size argument to $sample must not be negative
。要注意的是,當值超過集合中的文檔數量時,返回結果是集合中的所有文檔,但文檔順序是隨機的。
project
$project
的作用是過濾文檔中的欄位,這與投影操作相似,但處理結果將會傳入到下一個階段 。其語法格式如下:
{ $project: { <specification(s)> } }
準備以下數據:
> db.projects.save( {_id: 1, title: "籃球訓練營青春校園活動開始啦", numb: "A829Sck23", author: {last: "quinn", first: "James"}, hot: 35} )
假設 Pipeline
中的下一個 Stage
只需要文檔中的 title
和 author
欄位,對應示例如下:
> db.projects.aggregate([{$project: {title: 1, author: 1}}]) { "_id" : 1, "title" : "籃球訓練營青春校園活動開始啦", "author" : { "last" : "quinn", "first" : "James" } }
0
和 1
可以同時存在。對應示例如下:
> db.projects.aggregate([{$project: {title: 1, author: 1, _id: 0}}]) { "title" : "籃球訓練營青春校園活動開始啦", "author" : { "last" : "quinn", "first" : "James" } }
true
等效於 1
,false
等效於 0
,也可以混用布爾值和數字,對應示例如下:
> db.projects.aggregate([{$project: {title: 1, author: true, _id: false}}]) { "title" : "籃球訓練營青春校園活動開始啦", "author" : { "last" : "quinn", "first" : "James" } }
如果想要排除指定欄位,那麼在 $project
中將其設置為 0
或 false
即可,對應示例如下:
> db.projects.aggregate([{$project: {author: false, _id: false}}]) { "title" : "籃球訓練營青春校園活動開始啦", "numb" : "A829Sck23", "hot" : 35 }
$project
也可以作用於嵌入式文檔。對於 author
欄位,有時候我們只需要 FirstName
或者 Lastname
,對應示例如下:
> db.projects.aggregate([{$project: {author: {"last": false}, _id: false, numb: 0}}]) { "title" : "籃球訓練營青春校園活動開始啦", "author" : { "first" : "James" }, "hot" : 35 }
這裡使用 {author: {"last": false}}
過濾掉 LastName
,但保留 first
。
以上就是 $project
的基本用法和作用介紹,更多與 $project
相關的知識可查閱官方文檔 $project[34]。
lookup
$lookup
的作用是對同一資料庫中的集合執行左外連接,其語法格式如下:
{ $lookup: { from: <collection to join>, localField: <field from the input documents>, foreignField: <field from the documents of the "from" collection>, as: <output array field> } }
左外連接類似與下面的偽 SQL 語句:
SELECT *, <output array field> FROM collection WHERE <output array field> IN ( SELECT * FROM <collection to join> WHERE <foreignField>= <collection.localField>);
lookup
支援的指令及對應描述如下:
描述
準備以下數據:
> db.sav.insert([ { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 }, { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 }, { "_id" : 3 } ]) > db.avi.insert([ { "_id" : 1, "sku" : "almonds", description: "product 1", "instock" : 120 }, { "_id" : 2, "sku" : "bread", description: "product 2", "instock" : 80 }, { "_id" : 3, "sku" : "cashews", description: "product 3", "instock" : 60 }, { "_id" : 4, "sku" : "pecans", description: "product 4", "instock" : 70 }, { "_id" : 5, "sku": null, description: "Incomplete" }, { "_id" : 6 } ])
假設要連接集合 sav
中的 item
和集合 avi
中的 sku
,並將連接結果命名為 savi
。對應示例如下:
> db.sav.aggregate([ { $lookup: { from: "avi", localField: "item", foreignField: "sku", as: "savi" } } ])
命令執行後,輸出如下內容:
{ "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2, "savi" : [ { "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 } ] } { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1, "savi" : [ { "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 } ] } { "_id" : 3, "savi" : [ { "_id" : 5, "sku" : null, "description" : "Incomplete" }, { "_id" : 6 } ] }
上面的連接操作等效於下面這樣的偽 SQL:
SELECT *, savi FROM sav WHERE savi IN (SELECT * FROM avi WHERE sku= sav.item);
以上就是 lookup
的基本用法和作用介紹,更多與 lookup
相關的知識可查閱官方文檔 lookup[35]。
unwind
unwind
能將包含數組的文檔拆分稱多個文檔,其語法格式如下:
{ $unwind: { path: <field path>, includeArrayIndex: <string>, preserveNullAndEmptyArrays: <boolean> } }
unwind
支援的指令及對應描述如下:
指令 |
類型 |
描述 |
---|---|---|
path |
string |
指定數組欄位的欄位路徑, 必填。 |
includeArrayIndex |
string |
用於保存元素的數組索引的新欄位的名稱。 |
preserveNullAndEmptyArrays |
boolean |
默認情況下,如果path為 null、缺少該欄位或空數組, 則不輸出文檔。反之,將其設為 true 則會輸出文檔。 |
在開始學習之前,我們需要準備以下數據:
> db.shoes.save({_id: 1, brand: "Nick", sizes: [37, 38, 39]})
集合 shoes
中的 sizes
是一個數組,裡面有多個尺碼數據。假設要將這個文檔拆分成 3 個 size
為單個值的文檔,對應示例如下:
> db.shoes.aggregate([{$unwind : "$sizes"}]) { "_id" : 1, "brand" : "Nick", "sizes" : 37 } { "_id" : 1, "brand" : "Nick", "sizes" : 38 } { "_id" : 1, "brand" : "Nick", "sizes" : 39 }
顯然,這樣的文檔更方便我們做數據處理。preserveNullAndEmptyArrays
指令默認為 false
,也就是說文檔中指定的 path
為空、null
或缺少該 path
的時候,會忽略掉該文檔。假設數據如下:
> db.shoes2.insertMany([ {"_id": 1, "item": "ABC", "sizes": ["S", "M", "L"]}, {"_id": 2, "item": "EFG", "sizes": [ ]}, {"_id": 3, "item": "IJK", "sizes": "M"}, {"_id": 4, "item": "LMN" }, {"_id": 5, "item": "XYZ", "sizes": null} ])
我們執行以下命令:
> db.shoes2.aggregate([{$unwind: "$sizes"}])
就會得到如下輸出:
{ "_id" : 1, "item" : "ABC", "sizes" : "S" } { "_id" : 1, "item" : "ABC", "sizes" : "M" } { "_id" : 1, "item" : "ABC", "sizes" : "L" } { "_id" : 3, "item" : "IJK", "sizes" : "M" }
_id
為 2
、4
和 5
的文檔由於滿足 preserveNullAndEmptyArrays
的條件,所以不會被拆分。
以上就是 unwind
的基本用法和作用介紹,更多與 unwind
相關的知識可查閱官方文檔 unwind[36]。
out
out
的作用是聚合 Pipeline
返回的結果文檔,並將其寫入指定的集合。要注意的是,out
操作必須出現在 Pipeline
的最後。out
語法格式如下:
{ $out: "<output-collection>" }
準備以下數據:
> db.books.insertMany([ { "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 }, { "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 }, { "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 }, { "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 }, { "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 } ])
假設要集合 books
的分組結果保存到名為 books_result
的集合中,對應示例如下:
> db.books.aggregate([ ... { $group : {_id: "$author", books: {$push: "$title"}}}, ... { $out : "books_result" } ... ])
命令執行後,MongoDB 將會創建 books_result
集合,並將分組結果保存到該集合中。集合 books_result
中的文檔如下:
{ "_id" : "Homer", "books" : [ "The Odyssey", "Iliad" ] } { "_id" : "Dante", "books" : [ "The Banquet", "Divine Comedy", "Eclogues" ] }
以上就是 out
的基本用法和作用介紹,更多與 out
相關的知識可查閱官方文檔 out[37]。
Map-Reduce
Map-reduce 用於將大量數據壓縮為有用的聚合結果,其語法格式如下:
db.runCommand( { mapReduce: <collection>, map: <function>, reduce: <function>, finalize: <function>, out: <output>, query: <document>, sort: <document>, limit: <number>, scope: <document>, jsMode: <boolean>, verbose: <boolean>, bypassDocumentValidation: <boolean>, collation: <document>, writeConcern: <document> } )
其中,db.runCommand({mapReduce: <collection>})
也可以寫成 db.collection.mapReduce()
。各指令的對應描述如下:
指令 |
類型 |
描述 |
---|---|---|
mapReduce |
collection |
集合名稱,必填。 |
map |
function |
JavaScript 函數,必填。 |
reduce |
function |
JavaScript 函數,必填。 |
out |
string or document |
指定輸出結果,必填。 |
query |
document |
查詢條件語句。 |
sort |
document |
對文檔進行排序。 |
limit |
number |
指定輸入到 map 中的最大文檔數量。 |
finalize |
function |
修改 reduce 的輸出。 |
scope |
document |
指定全局變數。 |
jsMode |
boolean |
是否在執行map和reduce 函數之間將中間數據轉換為 BSON 格式,默認 false。 |
verbose |
boolean |
結果中是否包含 timing 資訊,默認 false。 |
bypassDocumentValidation |
boolean |
是否允許 mapReduce[38]在操作期間繞過文檔驗證,默認 false。 |
collation |
document |
指定要用於操作的排序規則[39]。 |
writeConcern |
document |
指定寫入級別,不填寫則使用默認級別。 |
簡單的 mapReduce
一個簡單的 mapReduce
語法示例如下:
var mapFunction = function() { ... }; var reduceFunction = function(key, values) { ... }; db.runCommand( ... { ... ... mapReduce: <input-collection>, ... ... map: mapFunction, ... ... reduce: reduceFunction, ... ... out: { merge: <output-collection> }, ... ... query: <query> ... })
map
函數負責將每個輸入的文檔轉換為零個或多個文檔。map
結構如下:
function() { ... emit(key, value); }
emit
函數的作用是分組,它接收兩個參數:
•key
:指定用於分組的欄位。•value
:要聚合的欄位。
在 map
中可以使用 this
關鍵字引用當前文檔。reduce
結構如下:
function(key, values) { ... return result; }
reduce
執行具體的數據處理操作,它接收兩個參數:
•key
:與 map
中的 key
相同,即分組欄位。•values
:根據分組欄位,將相同 key
的值放到同一個數組,values
就是包含這些分類數組的對象。
out
用於指定結果輸出,out: <collectionName>
會將結果輸出到新的集合,或者使用以下語法將結果輸出到已存在的集合中:
out: { <action>: <collectionName> [, db: <dbName>] [, sharded: <boolean> ] [, nonAtomic: <boolean> ] }
要注意的是,如果 out
指定的 collection
已存在,那麼它就會覆蓋該集合。在開始學習之前,我們需要準備以下數據:
> db.mprds.insertMany([ ... {_id: 1, numb: 3, score: 9, team: "B"}, ... {_id: 2, numb: 6, score: 9, team: "A"}, ... {_id: 3, numb: 24, score: 9, team: "A"}, ... {_id: 4, numb: 6, score: 8, team: "A"} ... ])
接著定義 map
函數、reduce
函數,並將其應用到集合 mrexample
上。然後為輸出結果指定存放位置,這裡將輸出結果存放在名為 mrexample_result
的集合中。
> var func_map = function(){emit(this.numb, this.score);}; > var func_reduce = function(key, values){return Array.sum(values);}; > db.mprds.mapReduce(func_map, func_reduce, {query: {team: "A"}, out: "mprds_result"})
map
函數指定了結果中包含的兩個鍵,並將 this.class
相同的文檔輸出到同一個文檔中。reduce
則對傳入的列表進行求和,求和結果作為結果中的 value
。命令執行完畢後,結果會被存放在集合 mprds_result
中。用以下命令查看結果:
> db.mprds_result.find() { "_id" : 6, "value" : 17 } { "_id" : 24, "value" : 9 }
結果文檔中的 _id
即 map
中的 this.numb
,value
為 reduce
函數的返回值。
下圖描述了此次 mapReduce
操作的完整過程:

在這裡插入圖片描述
finallize 剪枝
finallize
用於修改 reduce
的輸出結果,其語法格式如下:
function(key, reducedValue) { ... return modifiedObject; }
它接收兩個參數:
key
,與 map
中的 key
相同,即分組欄位。
reducedValue
,一個 Obecjt
,是reduce
的輸出。
上面我們介紹了 map
和 reduce
,並通過一個簡單的示例了解 mapReduce
的基本組成和用法。實際上我們還可以編寫功能更豐富的 reduce
函數,甚至使用 finallize
修改 reduce
的輸出結果。以下 reduce
函數將傳入的 values
進行計算和重組,返回一個 reduceVal
對象:
> var func_reduce2 = function(key, values){ reduceVal = {team: key, score: values, total: Array.sum(values), count: values.length}; return reduceVal; };
reduceVal
對象中包含 team
、score
、total
和 count
四個屬性。但我們還想為其添加 avg
屬性,那麼可以在 finallize
函數中執行 avg
值的計算和 avg
屬性的添加工作:
> var func_finalize = function(key, values){ values.avg = values.total / values.count; return values; };
map
保持不變,將這幾個函數作用於集合 mprds
上,對應示例如下:
> db.mprds.mapReduce(func_map, func_reduce2, {query: {team: "A"}, out: "mprds_result", finalize: func_finalize})
命令執行後,結果會存入指定的集合中。此時,集合 mprds_result
內容如下:
{ "_id" : 6, "value" : { "team" : 6, "score" : [ 9, 8 ], "total" : 17, "count" : 2, "avg" : 8.5 } } { "_id" : 24, "value" : 9 }
下圖描述了此次 mapReduce
操作的完整過程:

在這裡插入圖片描述
finallize
在 reduce
後面使用,微調 reduce
的處理結果。這著看起來像是一個園丁在修剪花圃的枝丫,所以人們將 finallize
形象地稱為「剪枝」。
要注意的是:map
會將 key
值相同的文檔中的 value
歸納到同一個對象中,這個對象會經過 reduce
和 finallize
。對於 key
值唯一的那些文檔,指定的 key
和 value
會被直接輸出。
簡單的聚合
除了 Aggregation Pipeline 和 Map-Reduce 這些複雜的聚合操作之外,MongoDB 還支援一些簡單的聚合操作,例如 count
、group
和 distinct
等。
count
count
用於計算集合或視圖中的文檔數,返回一個包含計數結果和狀態的文檔。其語法格式如下:
{ count: <collection or view>, query: <document>, limit: <integer>, skip: <integer>, hint: <hint>, readConcern: <document> }
count
支援的指令及對應描述如下:
指令 |
類型 |
描述 |
---|---|---|
count |
string |
要計數的集合或視圖的名稱,必填。 |
query |
document |
查詢條件語句。 |
limit |
integer |
指定要返回的最大匹配文檔數。 |
skip |
integer |
指定返回結果之前要跳過的匹配文檔數。 |
hint |
string or document |
指定要使用的索引,將索引名稱指定為字元串或索引規範文檔。 |
假設要統計集合 mprds
中的文檔數量,對應示例如下:
> db.runCommand({count: 'mprds'}) { "n" : 4, "ok" : 1 }
假設要統計集合 mprds
中 numb
為 6
的文檔數量,對應示例如下:
> db.runCommand({count: 'mprds', query: {numb: {$eq: 6}}}) { "n" : 2, "ok" : 1 }
指定返回結果之前跳過 1
個文檔,對應示例如下:
> db.runCommand({count: 'mprds', query: {numb: {$eq: 6}}, skip: 1}) { "n" : 1, "ok" : 1 }
更多關於 count
的知識可查閱官方文檔 Count[40]。
group
group
的作用是按指定的鍵對集合中的文檔進行分組,並執行簡單的聚合函數,它與 SQL 中的 SELECT ... GROUP BY
類似。其語法格式如下:
{ group: { ns: <namespace>, key: <key>, $reduce: <reduce function>, $keyf: <key function>, cond: <query>, finalize: <finalize function> } }
group
支援的指令及對應描述如下:
指令 |
類型 |
描述 |
---|---|---|
ns |
string |
通過操作執行組的集合,必填。 |
key |
ducoment |
要分組的欄位或欄位,必填。 |
$reduce |
function |
在分組操作期間對文檔進行聚合操作的函數。該函數有兩個參數:當前文檔和該組的聚合結果文檔。必填。 |
initial |
document |
初始化聚合結果文檔, 必填。 |
$keyf |
function |
替代 key。指定用於創建「密鑰對象」以用作分組密鑰的函數。使用$keyf而不是 key按計算欄位而不是現有文檔欄位進行分組。 |
cond |
document |
用於確定要處理的集合中的哪些文檔的選擇標準。如果省略,group 會處理集合中的所有文檔。 |
finalize |
function |
在返回結果之前運行,此函數可以修改結果文檔。 |
準備以下數據:
> db.sales.insertMany([ {_id: 1, orderDate: ISODate("2012-07-01T04:00:00Z"), shipDate: ISODate("2012-07-02T09:00:00Z"), attr: {name: "新款椰子鞋", price: 2999, size: 42, color: "香檳金"}}, {_id: 2, orderDate: ISODate("2012-07-03T05:20:00Z"), shipDate: ISODate("2012-07-04T09:00:00Z"), attr: {name: "高邦籃球鞋", price: 1999, size: 43, color: "獅王棕"}}, {_id: 3, orderDate: ISODate("2012-07-03T05:20:10Z"), shipDate: ISODate("2012-07-04T09:00:00Z"), attr: {name: "新款椰子鞋", price: 2999, size: 42, color: "香檳金"}}, {_id: 4, orderDate: ISODate("2012-07-05T15:11:33Z"), shipDate: ISODate("2012-07-06T09:00:00Z"), attr: {name: "極速跑鞋", price: 500, size: 43, color: "西湖藍"}}, {_id: 5, orderDate: ISODate("2012-07-05T20:22:09Z"), shipDate: ISODate("2012-07-06T09:00:00Z"), attr: {name: "新款椰子鞋", price: 2999, size: 42, color: "香檳金"}}, {_id: 6, orderDate: ISODate("2012-07-05T22:35:20Z"), shipDate: ISODate("2012-07-06T09:00:00Z"), attr: {name: "透氣網跑", price: 399, size: 38, color: "玫瑰紅"}} ])
假設要將集合 sales
中的文檔按照 attr.name
進行分組,並限定參與分組的文檔的 shipDate
大於指定時間。對應示例如下:
> db.runCommand({ group:{ ns: 'sales', key: {"attr.name": 1}, cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}}, $reduce: function(curr, result){}, initial: {} } })
命令執行後,會返回一個結果檔。其中, retval
包含指定欄位 attr.name
的數據,count
為參與分組的文檔數量,keys
代表組的數量,ok
代表文檔狀態。結果文檔如下:
{ "retval" : [ { "attr.name" : "高邦籃球鞋" }, { "attr.name" : "新款椰子鞋" }, { "attr.name" : "極速跑鞋" }, { "attr.name" : "透氣網跑" } ], "count" : NumberLong(5), "keys" : NumberLong(4), "ok" : 1 }
上方示例指定的 key
是 attr.name
。由於參與分組的 5 個文檔中只有 2 個文檔的 attr.name
是相同的,所以分組結果中的 keys
為 4
,這代表集合 sales
中的文檔被分成了 4 組。
將 attr.name
換成 shipDate
,看看結果會是什麼。對應示例如下:
> db.runCommand( { group:{ ns: 'sales', key: {shipDate: 1}, cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}}, $reduce: function(curr, result){}, initial: {} } } )
命令執行後,返回如下結果:
{ "retval" : [ { "shipDate" : ISODate("2012-07-04T09:00:00Z") }, { "shipDate" : ISODate("2012-07-06T09:00:00Z") } ], "count" : NumberLong(5), "keys" : NumberLong(2), "ok" : 1 }
由於參與分組的 5 個文檔中有幾個文檔的 shipDate
是重複的,所以分組結果中的 keys
為 2
,這代表集合 sales
中的文檔被分成了 2 組。
上面的示例並沒有用到 reduce
、 initial
和 finallize
,接下來我們將演示它們的用法和作用。假設要統計同組的銷售總額,那麼可以在 reduce
中執行具體的計算邏輯。對應示例如下:
> db.runCommand( { group:{ ns: 'sales', key: {shipDate: 1}, cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}}, $reduce: function(curr, result){ result.total += curr.attr.price; }, initial: {total: 0} } } )
命令執行後,返回結果如下:
{ "retval" : [ { "shipDate" : ISODate("2012-07-04T09:00:00Z"), "total" : 4998 }, { "shipDate" : ISODate("2012-07-06T09:00:00Z"), "total" : 3898 } ], "count" : NumberLong(5), "keys" : NumberLong(2), "ok" : 1 }
人工驗證一下,發貨日期 shipDate
大於 2012-07-04T09:00:00Z
的文檔為:
{ "_id" : 2, "orderDate" : ISODate("2012-07-03T05:20:00Z"), "shipDate" : ISODate("2012-07-04T09:00:00Z"), "attr" : { "name" : "高邦籃球鞋", "price" : 1999, "size" : 43, "color" : "獅王棕" } } { "_id" : 3, "orderDate" : ISODate("2012-07-03T05:20:10Z"), "shipDate" : ISODate("2012-07-04T09:00:00Z"), "attr" : { "name" : "新款椰子鞋", "price" : 2999, "size" : 42, "color" : "香檳金" } }
銷售總額為 1999 + 2999 = 4998
,與返回結果相同。發貨日期 shipDate
大於 2012-07-06T09:00:00Z
的文檔為:
{ "_id" : 4, "orderDate" : ISODate("2012-07-05T15:11:33Z"), "shipDate" : ISODate("2012-07-06T09:00:00Z"), "attr" : { "name" : "極速跑鞋", "price" : 500, "size" : 43, "color" : "西湖藍" } } { "_id" : 5, "orderDate" : ISODate("2012-07-05T20:22:09Z"), "shipDate" : ISODate("2012-07-06T09:00:00Z"), "attr" : { "name" : "新款椰子鞋", "price" : 2999, "size" : 42, "color" : "香檳金" } } { "_id" : 6, "orderDate" : ISODate("2012-07-05T22:35:20Z"), "shipDate" : ISODate("2012-07-06T09:00:00Z"), "attr" : { "name" : "透氣網跑", "price" : 399, "size" : 38, "color" : "玫瑰紅" } }
銷售總額為 500 + 2999 + 399 = 3898
,與返回結果相同。
有時候可能需要統計每個組的文檔數量以及計算平均銷售額,對應示例如下:
> db.runCommand( { group:{ ns: 'sales', key: {shipDate: 1}, cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}}, $reduce: function(curr, result){ result.total += curr.attr.price; result.count ++; }, initial: {total: 0, count: 0}, finalize: function(result){ result.avg = Math.round(result.total / result.count); } } } )
上面的示例中改動了 $reduce
函數,目的是為了統計 count
。然後新增了 finalize
,目的是計算分組中的平均銷售額。命令執行後,返回以下文檔:
{ "retval" : [ { "shipDate" : ISODate("2012-07-04T09:00:00Z"), "total" : 4998, "count" : 2, "avg" : 2499 }, { "shipDate" : ISODate("2012-07-06T09:00:00Z"), "total" : 3898, "count" : 3, "avg" : 1299 } ], "count" : NumberLong(5), "keys" : NumberLong(2), "ok" : 1 }
以上就是 group
的基本用法和作用介紹,更多與 group
相關的知識可查閱官方文檔 group[41]。
distinct
distinct
的作用是查找單個集合中指定欄位的不同值,其語法格式如下:
{ distinct: "<collection>", key: "<field>", query: <query>, readConcern: <read concern document>, collation: <collation document> }
distinct
支援的指令及對應描述如下:
指令 |
類型 |
描述 |
---|---|---|
distinct |
string |
集合名稱, 必填。 |
key |
string |
指定的欄位, 必填。 |
query |
document |
查詢條件語句。 |
readConcern |
document |
|
collation |
document |
|
準備以下數據:
> db.dress.insertMany([ ... {_id: 1, "dept": "A", attr: {"款式": "立領", color: "red" }, sizes: ["S", "M" ]}, ... {_id: 2, "dept": "A", attr: {"款式": "圓領", color: "blue" }, sizes: ["M", "L" ]}, ... {_id: 3, "dept": "B", attr: {"款式": "圓領", color: "blue" }, sizes: "S" }, ... {_id: 4, "dept": "A", attr: {"款式": "V領", color: "black" }, sizes: ["S" ] } ])
假設要統計集合 dress
中所有文檔的 dept
欄位的不同值,對應示例如下:
> db.runCommand ( { distinct: "dress", key: "dept" } ) { "values" : [ "A", "B" ], "ok" : 1 }
或者看看有那些款式,對應示例如下
> db.runCommand ( { distinct: "dress", key: "attr.款式" } ) { "values" : [ "立領", "圓領", "V領" ], "ok" : 1 }
就算值是數組, distinct
也能作出正確處理,對應示例如下:
> db.runCommand ( { distinct: "dress", key: "sizes" } ) { "values" : [ "M", "S", "L" ], "ok" : 1 }
流式聚合操作小結
以上就是本篇對 MongoDB 中流式聚合操作的介紹。聚合與管道的概念並不常見,但是理解起來也不難。只要跟著示例思考,並動手實踐,相信你很快就能夠熟練掌握聚合操作。
References
[1]
$addFields
: https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/#pipe._S_addFields [2]
$bucket
: https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/#pipe._S_bucket [3]
$bucketAuto
: https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#pipe._S_bucketAuto [4]
$collStats
: https://docs.mongodb.com/manual/reference/operator/aggregation/collStats/#pipe._S_collStats [5]
$count
: https://docs.mongodb.com/manual/reference/operator/aggregation/count/#pipe._S_count [6]
$facet
: https://docs.mongodb.com/manual/reference/operator/aggregation/facet/#pipe._S_facet [7]
$geoNear
: https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#pipe._S_geoNear [8]
$graphLookup
: https://docs.mongodb.com/manual/reference/operator/aggregation/graphLookup/#pipe._S_graphLookup [9]
$group
: https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group [10]
$indexStats
: https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/#pipe._S_indexStats [11]
$limit
: https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit [12]
$listSessions
: https://docs.mongodb.com/manual/reference/operator/aggregation/listSessions/#pipe._S_listSessions [13]
$lookup
: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup [14]
$match
: https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match [15]
$out
: https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out [16]
$project
: https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project [17]
$redact
: https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact [18]
$replaceRoot
: https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/#pipe._S_replaceRoot [19]
$sample
: https://docs.mongodb.com/manual/reference/operator/aggregation/sample/#pipe._S_sample [20]
$skip
: https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip [21]
$sort
: https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort [22]
$sortByCount
: https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/#pipe._S_sortByCount [23]
$unwind
: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind [24]
$match
: https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match [25]
$group
: https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group [26]
$match
: https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match [27]
$project
: https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project [28]
$sort
: https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort [29]
$limit
: https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit [30]
$sum
: https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum [31]
$sum
: https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum [32]
$sortByCount
: https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/#pipe._S_sortByCount [33]
$lookup
: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup [34]
$project: https://docs.mongodb.com/manual/reference/operator/aggregation/project/#project-aggregation [35]
lookup: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#lookup-aggregation [36]
unwind: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#unwind-aggregation [37]
out: https://docs.mongodb.com/manual/reference/operator/aggregation/out/#out-aggregation [38]
mapReduce
: https://docs.mongodb.com/manual/reference/command/mapReduce/#dbcmd.mapReduce [39]
排序規則: https://docs.mongodb.com/manual/reference/bson-type-comparison-order/#collation [40]
Count: https://docs.mongodb.com/manual/reference/command/count/#count [41]
group: https://docs.mongodb.com/manual/reference/command/group/#group