­

硬貨來了!輕鬆掌握 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,它們是:

描述

文檔、StagePipeline 的關係如下圖所示:

在這裡插入圖片描述

上圖描述了文檔經過 $match$sample$project 等三個 Stage 並輸出的過程。SQL 中常見的聚合術語有 WHERESUMCOUNT 等。下表描述了常見的 SQL 聚合術語、函數和概念以及對應的 MongoDB 操作符或 Stage

MongoDB

下面,我們將通過示例了解 AggregateStagePipeline 之間的關係。

概念淺出

$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 }  ... ])

然後我們建立只有一個 StagePipeline,以實現過濾出 authordave 的文檔。對應示例如下:

> db.artic.aggregate([  ... {$match: {author: "dave"}}  ... ])  { "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 }  { "_id" : 2, "author" : "dave", "score" : 85, "views" : 521 }

如果要建立有兩個 StagePipeline,那麼就在 aggregate 中添加一個 Stage 即可。現在有這樣一個需求:統計集合 articscore 大於 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 }

這個示例的完整過程可以用下圖表示:

在這裡插入圖片描述

通過上面的描述和舉例,我相信你對 AggregateStagePipeline 有了一定的了解。接下來,我們將學習常見的 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 只需要文檔中的 titleauthor 欄位,對應示例如下:

> db.projects.aggregate([{$project: {title: 1, author: 1}}])  { "_id" : 1, "title" : "籃球訓練營青春校園活動開始啦", "author" : { "last" : "quinn", "first" : "James" } }

01 可以同時存在。對應示例如下:

> db.projects.aggregate([{$project: {title: 1, author: 1, _id: 0}}])  { "title" : "籃球訓練營青春校園活動開始啦", "author" : { "last" : "quinn", "first" : "James" } }

true 等效於 1false 等效於 0,也可以混用布爾值和數字,對應示例如下:

> db.projects.aggregate([{$project: {title: 1, author: true, _id: false}}])  { "title" : "籃球訓練營青春校園活動開始啦", "author" : { "last" : "quinn", "first" : "James" } }

如果想要排除指定欄位,那麼在 $project 中將其設置為 0false 即可,對應示例如下:

> 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" }

_id245 的文檔由於滿足 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 }

結果文檔中的 _idmap 中的 this.numbvaluereduce 函數的返回值。

下圖描述了此次 mapReduce 操作的完整過程:

在這裡插入圖片描述

finallize 剪枝

finallize 用於修改 reduce 的輸出結果,其語法格式如下:

function(key, reducedValue) {     ...     return modifiedObject;  }

它接收兩個參數:

key,與 map 中的 key 相同,即分組欄位。

reducedValue,一個 Obecjt,是reduce 的輸出。

上面我們介紹了 mapreduce,並通過一個簡單的示例了解 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 對象中包含 teamscoretotalcount 四個屬性。但我們還想為其添加 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 操作的完整過程:

在這裡插入圖片描述

finallizereduce 後面使用,微調 reduce 的處理結果。這著看起來像是一個園丁在修剪花圃的枝丫,所以人們將 finallize 形象地稱為「剪枝」。

要注意的是:map 會將 key 值相同的文檔中的 value 歸納到同一個對象中,這個對象會經過 reducefinallize。對於 key 值唯一的那些文檔,指定的 keyvalue 會被直接輸出。

簡單的聚合

除了 Aggregation Pipeline 和 Map-Reduce 這些複雜的聚合操作之外,MongoDB 還支援一些簡單的聚合操作,例如 countgroupdistinct 等。

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 }

假設要統計集合 mprdsnumb6 的文檔數量,對應示例如下:

> 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  }

上方示例指定的 keyattr.name。由於參與分組的 5 個文檔中只有 2 個文檔的 attr.name 是相同的,所以分組結果中的 keys4,這代表集合 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 是重複的,所以分組結果中的 keys2,這代表集合 sales 中的文檔被分成了 2 組。

上面的示例並沒有用到 reduceinitialfinallize ,接下來我們將演示它們的用法和作用。假設要統計同組的銷售總額,那麼可以在 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