「簡明教程」輕鬆掌握 MongDB 流式聚合操做

信息科學中的聚合是指對相關數據進行內容篩選、處理和歸類並輸出結果的過程。MongoDB 中的聚合是指同時對多個文檔中的數據進行處理、篩選和歸類並輸出結果的過程。數據在聚合操做的過程當中,就像是水流過一節一節的管道同樣,因此 MongoDB 中的聚合又被人稱爲流式聚合。mongodb

MongoDB 提供了幾種聚合方式:數據庫

  • Aggregation Pipeline
  • Map-Reduce
  • 簡單聚合

接下來,咱們將全方位地瞭解 MongoDB 中的聚合。數組

Aggregation Pipeline

Aggregation Pipeline 又稱聚合管道。開發者能夠將多個文檔傳入一個由多個 Stage 組成的 Pipeline,每個 Stage 處理的結果將會傳入下一個 Stage 中,最後一個 Stage 的處理結果就是整個 Pipeline 的輸出。bash

建立聚合管道的語法以下:微信

db.collection.aggregate( [ { <stage> }, ... ] )
複製代碼

MongoDB 提供了 23 種 Stage,它們是:session

Stage 描述
$addFields 向文檔添加新字段。
$bucket 根據指定的表達式和存儲區邊界將傳入的文檔分組。
$bucketAuto 根據指定的表達式將傳入的文檔分類爲特定數量的組,自動肯定存儲區邊界。
$collStats 返回有關集合或視圖的統計信息。
$count 返回聚合管道此階段的文檔數量計數。
$facet 在同一組輸入文檔的單個階段內處理多個聚合操做。
$geoNear 基於與地理空間點的接近度返回有序的文檔流。
$graphLookup 對集合執行遞歸搜索。
$group 按指定的標識符表達式對文檔進行分組。
$indexStats 返回集合的索引信息。
$limit 將未修改的前 n 個文檔傳遞給管道。
$listSessions 列出system.sessions集合的全部會話。
$lookup 對同一數據庫中的另外一個集合執行左外鏈接。
$match 過濾文檔,僅容許匹配的文檔地傳遞到下一個管道階段。
$out 將聚合管道的結果文檔寫入指定集合,它必須是管道中的最後一個階段。
$project 爲文檔添加新字段或刪除現有字段。
$redact 可用於實現字段級別的編輯。
$replaceRoot 用指定的嵌入文檔替換文檔。該操做將替換輸入文檔中的全部現有字段,包括_id字段。指定嵌入在輸入文檔中的文檔以將嵌入文檔提高到頂層。
$sample 從輸入中隨機選擇指定數量的文檔。
$skip 跳過前 n 個文檔,並將未修改的其他文檔傳遞到下一個階段。
$sort 按指定的排序鍵從新排序文檔流。只有訂單改變; 文件保持不變。對於每一個輸入文檔,輸出一個文檔。
$sortByCount 對傳入文檔進行分組,而後計算每一個不一樣組中的文檔計數。
$unwind 解構文檔中的數組字段。

文檔、StagePipeline 的關係以下圖所示:函數

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

SQL MongoDB
WHERE $match
GROUP BY $group
HAVING $match
SELECT $project
ORDER BY $sort
LIMIT $limit
SUM() $sum
COUNT() $sum$sortByCount
join $lookup

下面,咱們將經過示例瞭解 AggregateStagePipeline 之間的關係。學習

概念淺出

$match 的描述爲「過濾文檔,僅容許匹配的文檔地傳遞到下一個管道階段」。其語法格式以下:優化

{ $match: { <query> } }
複製代碼

在開始學習以前,咱們須要準備如下數據:ui

> 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

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 支持的指令及對應描述以下:

領域 描述
from 指定集合名稱。
localField 指定輸入 $lookup 中的字段。
foreignField 指定from 給定的集合中的文檔字段。
as 指定要添加到輸入文檔的新數組字段的名稱。
新數組字段包含from集合中的匹配文檔。
若是輸入文檔中已存在指定的名稱,則會覆蓋現有字段 。

準備如下數據:

> 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

unwind

unwind 能將包含數組的文檔拆分稱多個文檔,其語法格式以下:

{
  $unwind:
    {
      path: <field path>,
      includeArrayIndex: <string>,
      preserveNullAndEmptyArrays: <boolean>
    }
}
複製代碼

unwind 支持的指令及對應描述以下:

指令 類型 描述
path string 指定數組字段的字段路徑, 必填。
includeArrayIndex string 用於保存元素的數組索引的新字段的名稱。
preserveNullAndEmptyArrays boolean 默認狀況下,若是pathnull、缺乏該字段或空數組, 則不輸出文檔。反之,將其設爲 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

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

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 是否在執行mapreduce 函數之間將中間數據轉換爲 BSON 格式,默認 false
verbose boolean 結果中是否包含 timing 信息,默認 false
bypassDocumentValidation boolean 是否容許 mapReduce在操做期間繞過文檔驗證,默認 false
collation document 指定要用於操做的排序規則
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

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

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 中流式聚合操做的介紹。聚合與管道的概念並不常見,可是理解起來也不難。只要跟着示例思考,並動手實踐,相信你很快就可以熟練掌握聚合操做。

看不夠?

除了幫助你掌握流式聚合操做以外,我還寫了一整套 MongoDB 快速入門教程,看完教程你將收穫:

  • 文檔的 CRUD 操做和 Cursor 對象
  • 掌握流式聚合操做,輕鬆面對任何數據處理需求
  • 瞭解 MongoDB 的查詢效率和優化
  • 學會提升 MongoDB 的可用性
  • 學會應對數據服務故障
  • 理解 MongoDB 的訪問控制
  • 學會用數據模型下降數據冗餘,提升效率
  • 掌握 mongodump 數據備份與還原方法

MongoDB 系列教程適合人羣

  • 對 MongoDB 感興趣的 0 基礎開發者
  • 有必定基礎,想要全面瞭解 MongoDB 的開發者

爲何選擇這套 MongoDB 教程?

  • 內容相似的 MongoDB 教程動輒幾百元
  • MongoDB 官方文檔晦澀難懂
  • 網上其餘文章不夠全面,難以造成體系化知識
  • Chat 中的示例代碼能夠直接複製使用,方便練習
  • MongoDB 內容繁多,自學者不肯定須要學習哪些知識

這是寫給 0 基礎同窗的 MongoDB 快速入門文章。內容從文檔 CRUD 到流式聚合操做;從執行計劃、索引、數據模型到複製集;從分片、訪問控制到數據備份與還原。全篇近 5 萬詞的內容覆蓋了 MongoDB 的大部分知識點,徹底知足平常開發的要求。

先聲明,這套教程 9.9 元。

你能夠添加做者微信:DomFreez,並向做者吐槽。

目前已有 600+ 朋友參與了哦

相關文章
相關標籤/搜索