1. 程式人生 > 程式設計 >「簡明教程」輕鬆掌握 MongDB 流式聚合操作

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

資訊科學中的聚合是指對相關資料進行內容篩選、處理和歸類並輸出結果的過程。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 描述
$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> } }
複製程式碼

在開始學習之前,我們需要準備以下資料:

> db.artic.insertMany([
... { "_id" : 1,"author" : "dave","score" : 80,"views" : 100 },... { "_id" : 2,"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,"views" : 100 }
{ "_id" : 2,"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,"views" : 100 }
{ "_id" : 3,"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" : "籃球訓練營青春校園活動開始啦","first" : "James" } }
複製程式碼

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

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

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

> db.projects.aggregate([{$project: {author: false,"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 },"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,"quantity" : 2,"savi" : [
      { "_id" : 1,"description" : "product 1","instock" : 120 }
   ]
}
{
   "_id" : 2,"quantity" : 1,"savi" : [
      { "_id" : 4,"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,"sizes" : 38 }
{ "_id" : 1,"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,"sizes" : "M" }
{ "_id" : 1,"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","copies" : 1 },{ "_id" : 8645,"title" : "Eclogues",{ "_id" : 7000,"title" : "The Odyssey","author" : "Homer","copies" : 10 },{ "_id" : 7020,"title" : "Iliad","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,team: "A"},... {_id: 3,numb: 24,... {_id: 4,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,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>,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"),{_id: 4,orderDate: ISODate("2012-07-05T15:11:33Z"),shipDate: ISODate("2012-07-06T09:00:00Z"),attr: {name: "極速跑鞋",price: 500,color: "西湖藍"}},{_id: 5,orderDate: ISODate("2012-07-05T20:22:09Z"),{_id: 6,orderDate: ISODate("2012-07-05T22:35:20Z"),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},initial: {}
        }
	}
)
複製程式碼

命令執行後,返回如下結果:

{
	"retval" : [
		{
			"shipDate" : ISODate("2012-07-04T09:00:00Z")
		},{
			"shipDate" : ISODate("2012-07-06T09:00:00Z")
		}
	],"keys" : NumberLong(2),"ok" : 1
}
複製程式碼

由於參與分組的 5 個檔案中有幾個檔案的 shipDate 是重複的,所以分組結果中的 keys2,這代表集合 sales 中的檔案被分成了 2 組。

上面的示例並沒有用到 reduceinitialfinallize ,接下來我們將演示它們的用法和作用。假設要統計同組的銷售總額,那麼可以在 reduce 中執行具體的計算邏輯。對應示例如下:

> db.runCommand(
{
    group:{
        ns: 'sales',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
		}
	],"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"),"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,"color" : "西湖藍" } }
{ "_id" : 5,"orderDate" : ISODate("2012-07-05T20:22:09Z"),"color" : "香檳金" } }
{ "_id" : 6,"orderDate" : ISODate("2012-07-05T22:35:20Z"),"attr" : { "name" : "透氣網跑","price" : 399,"size" : 38,"color" : "玫瑰紅" } }
複製程式碼

銷售總額為 500 + 2999 + 399 = 3898,與返回結果相同。

有時候可能需要統計每個組的檔案數量以及計算平均銷售額,對應示例如下:

> db.runCommand(
{
    group:{
        ns: 'sales',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,"avg" : 2499
		},"total" : 3898,"count" : 3,"avg" : 1299
		}
	],"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" ]},attr: {"款式": "圓領",color: "blue" },sizes: ["M","L" ]},"dept": "B",sizes: "S" },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+ 朋友參與了哦