「簡明教程」輕鬆掌握 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 |
解構檔案中的陣列欄位。 |
檔案、Stage
和 Pipeline
的關係如下圖所示:
$match
、$sample
和 $project
等三個 Stage
並輸出的過程。SQL 中常見的聚合術語有 WHERE
、SUM
和 COUNT
等。下表描述了常見的 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 |
下面,我們將通過示例瞭解 Aggregate
、 Stage
和 Pipeline
之間的關係。
概念淺出
$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 }
... ])
複製程式碼
然後我們建立只有一個 Stage
的 Pipeline
,以實現過濾出 author
為 dave
的檔案。對應示例如下:
> db.artic.aggregate([
... {$match: {author: "dave"}}
... ])
{ "_id" : 1,"views" : 100 }
{ "_id" : 2,"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,"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
只需要檔案中的 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" : "籃球訓練營青春校園活動開始啦","first" : "James" } }
複製程式碼
true
等效於 1
,false
等效於 0
,也可以混用布林值和數字,對應示例如下:
> db.projects.aggregate([{$project: {title: 1,author: true,_id: false}}])
{ "title" : "籃球訓練營青春校園活動開始啦","first" : "James" } }
複製程式碼
如果想要排除指定欄位,那麼在 $project
中將其設定為 0
或 false
即可,對應示例如下:
> 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 | 預設情況下,如果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,"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" }
複製程式碼
_id
為 2
、4
和 5
的檔案由於滿足 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 | 是否在執行map 和reduce 函式之間將中間資料轉換為 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 }
複製程式碼
結果檔案中的 _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,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>,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。
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
}
複製程式碼
上方示例指定的 key
是 attr.name
。由於參與分組的 5 個檔案中只有 2 個檔案的 attr.name
是相同的,所以分組結果中的 keys
為 4
,這代表集合 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
是重複的,所以分組結果中的 keys
為 2
,這代表集合 sales
中的檔案被分成了 2 組。
上面的示例並沒有用到 reduce
、 initial
和 finallize
,接下來我們將演示它們的用法和作用。假設要統計同組的銷售總額,那麼可以在 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+ 朋友參與了哦