[MongoDB] 學習筆記(二) - 進階find使用、Aggregation
Tags: Aggregation, Mongodb

前一章說明了基本增刪改查的功能，這裡想更詳細的紀錄要如何使用find的方法查找，以及各method使用的operator，並說明如何透過aggregation進行進階分組查詢。

本章使用的資料可以在這裡找到，並使用 mongoimport 將其導入。

Mongo版本：v4.4.1

mongoimport C:/Users/user/Downloads/movie.json -d demo -c movies --jsonArray --drop

這個資料是一個電影的數據，document的數量是100，內容包括電影名稱、年分、導演、演員、IMDB的評分等等。

資料部分結果

目錄

find、findOne語法結構, Query Selectors and Projection Operators

Query Selectors - Comparison Operator

Query Selectors - Logical Operator

Query Selectors - Element Operator

Query Selectors - Array Operator & 矩陣的操作

Projection Operators - 輸出結果調整：矩陣調整

Cursor Methods - 修改query查詢結果

Aggregation Pipeline Stages、Operators 語法結構

Aggregation Pipeline Stages：$match、$project

Aggregation Pipeline Stages：$group、$bucket、$bucketAuto

Aggregation Pipeline Operators：$cond, $switch in $project, $group

Aggregation Pipeline Stages：$out 輸出查詢結果

結論

find、findOne語法結構, Query Selectors and Projection Operators

find的語法結構有兩個parameter，分別為 Query和 Projection，兩個都是選擇性的參數，如果不輸入， db.<collection_name>.find() 、 findOne() 會輸出所有、第一個document。

Query可以讓我們對資料進行條件篩選，例如想找分數等於8.5分的電影， db.<collection_name>.find({imdb_score:8.5}) 。而我們可以利用 query operators來進行細節的調控，如用 $gt 找分數大於8.5分， db.<collection_name>.find({imdb_score:{$gt: 8.5}}) 。

Projection可以讓我們控制回傳的結果，例如當以Query篩選出大於8.5分電影的document後，我只想看到document中的電影名稱，這時我就可以透過projection來篩選 field， db.<collection_name>.find({$gt: 8.5}, {title:1}) 。細節則是透過projection operators來操作。使用結構如下圖。

使用方法method包含find與findOne，而query與projection則是object的格式，可以使用大括號，列出多個field，並以逗點分隔，就能進行多條件篩選。如 db.<collection_name>.find({field:value, field:value, ...}, {field:value, field:value, ...})

當然，結構圖只是概況，實際上變化度是大於圖形內容的，後面再詳述。

Query Selectors - Comparison Operator

顧名思義，是篩選比較operator，內容包括 等於 $eq、不等於 $ne、大於$gt、大於等於 $gte、小於 $lt、小於等於 $lte、

等於 $eq：找尋imdb_scoore = 8.5

等於有兩種寫法，分別為 db.movies.findOne({imdb_score: 8.5}) 與 db.movies.findOne({imdb_score: {$eq: 8.5}}) ，後續為方便檢視結果，均加上projection，只顯示imdb_score。

不等於 $ne：找尋imdb_scoore != 8.5

db.movies.find({imdb_score: {$ne: 8.5}}, {_id:0, imdb_score:1 })

大於 $gt：找尋imdb_scoore >8.5

db.movies.find({imdb_score: {$gt: 8.5}}, {_id:0, imdb_score:1 })

大於等於 $gte：找尋imdb_scoore >= 8.5

db.movies.find({imdb_score: {$gte: 8.5}}, {_id:0, imdb_score:1 })

小於 $lt：找尋imdb_scoore < 5.5

db.movies.find({imdb_score: {$lt: 5.5}}, {_id:0, imdb_score:1 })

小於等於 $lte：找尋imdb_scoore <= 5.5

db.movies.find({imdb_score: {$lte: 5.5}}, {_id:0, imdb_score:1 })

在清單中 $in：找尋imdb_scoore 等於8.5與9.0

db.movies.find({imdb_score: {$in: [8.5, 9.0]}}, {_id:0, imdb_score:1 })

不在清單中 $nin：找尋imdb_scoore 不等於8.5與9.0

db.movies.find({imdb_score: {$nin: [8.5, 9.0]}}, {_id:0, imdb_score:1 })

Query Selectors - Logical Operator

有4個operator，其中同時滿足$and、滿足任一項 $or、不能滿足任一項 $nor，三個operator可以用來串聯多個條件。$not則是用於條件的反義。

同時滿足$and：找尋imdb_scoore 大於等於8.5與小於等於9.0

db.movies.find({$and: [{imdb_score: {$gte: 8}}, {imdb_score: {$lte: 8.5}}]}, {‘_id’:0, imdb_score: 1})

滿足其中一項 $or：找尋imdb_scoore 大於等於8.8與小於等於5.8

db.movies.find({$or: [{imdb_score: {$gte: 8.8}}, {imdb_score: {$lte: 5.8}}]}, {‘_id’:0, imdb_score: 1})

不能滿足任一項 $nor：找尋imdb_scoore 不滿足 大於等於8.5或 不滿足小於等於8

db.movies.find({$nor: [{imdb_score: {$gte: 8.5}}, {imdb_score: {$lte: 8}}]}, {‘_id’:0, imdb_score: 1})

不 $not: 找尋imdb_scoore不超過5.5

db.movies.find({imdb_score: {$not: {$gt: 5.5}}}, {‘_id’:0,imdb_score:1})

Query Selectors - Element Operator

這個系列的operator用來判斷document中field的屬性，有兩個成員，field在哪些document存在$exists、field在哪些document中符合指定屬性 $type。

這裡我先插入一個名為test的新Collection，內容是1個array中有3個documents，但不同document中有不同的filed數量與屬性，下面將使用element operator進行篩選。

db.test.drop()

db.test.insert(

[{var1: 1, var2: 'Ben'},

{var1: 2, var2: 44573},

{var1: 2, var2: null},

{var1: 3}]

)

field是否存在 $exists

如果要確定一個document中，是否具有某個field，可以使用 <$exists: 1> ；反之，如果要篩選不具有某個field的document，則可以用 <$exists: 0> ，如篩選有、沒有var2 field的document分別為： db.test.find({var2: {$exists: 1}}) 、 db.test.find({var2: {$exists: 0}}) 。

field 屬性的篩選 $type：

如果想要篩選某個field，為指定屬性，可以使用 <$type: type> ，如我想要篩選var2 是數字，則可以寫成 db.test.find({var2: {$type: 'number'}}) 。如果想要篩選多種屬性，例如想篩選var2屬性為數字與字串任一種的document，則可以用array表示， db.test.find({var2: {$type: ['number', 'string']}}) ，詳細mongo定義的類別，可以參考下列網站。

遺失值、null的篩選

null在mongo中的type的定義，其實就是'null'，所以可以寫成 db.test.find({var2: {$type: 'null'}})

Query Selectors - Array Operator & 矩陣的操作

當field中的值是一個array時，array中可能包含多個數值，此時使用一般的operator可能無法得到想要的結果，此時須使用Array Operator，包含三個，分別為$all、$eleMatch、$size。定義：

$all ：document中的array包含所有指給訂條件

$elemMatch：document中的array符合其中一項給定條件

$size：document中的array的size

以下示範取得各種元素條件的方法。先插入一個collection，內容的document中，有單純的string、有array、有document、巢狀array等形式。

db.demo_1.drop()

db.demo_1.insert(

[

{color: 'R'},

{color: ['R']},

{color: {var1: 'R'}},

{color: [['R']]},

{color: [[['R']]]},

{color: [{var1: 'R'}]},

{color: ['R', 'G']},

{color: ['G', 'R']},

{color: ['R', 'B']},

{color: [['R'], 'B']},

{color: [{var1: 'R'}, 'B']},

{color: [['R'], ['B']]},

{color: [{var1: 'R'}, {var2: 'B'}]},

{color: [['R'], {var2: 'B'}]},

{color: ['B', 'B', 'B']},

{color: ['R', 'G', 'B']}

])

在Array取得指定元素

如果我想在demo_1的collection中取得'R'，按照非array的篩選方式，會是 db.demo_1.find({color: “R” }, {_id:0}) ，這個結果取得了非array有R，以及array中第一階有R的document，但無法取得巢狀結構下的R。

db.demo_1.find({color: [“R”] }, {_id:0}) 這個方法篩選['R']，可以發現篩選到了value為['R']、與矩陣中巢型的第一階為['R']的對象。

但如果要篩選巢型document中的'R'時，就須以選擇元素的方式 'color.var' ， db.demo_1.find({‘color.var1’: “R” } 。

如果需要同時取得上面三種情況，則以 $or 串接 db.demo_1.find({$or: [{color: “R”}, {color: [“R”]}, {‘color.var1’: “R” }]}, {_id:0})

取得Array中指定位置的指定元素

如果是document的話，可以指定field來給定條件，而如果是array，則可以給定位置，像是如果要取得矩陣中第1個元素，field的位置可以寫成 <field>.0 ，第2個元素則是 <field>.1 ，以此類推。

db.demo_1.find({“color.0”: “R” }, {_id:0}) ，所以這個query篩選了array中第一個元素為R的document，而如果document根本不是array，則不會出現。所以下面結果沒有出現value為string或document的document。

當然，我也可以篩選array第一個元素的array的第一個元素， db.demo_1.find({“color.0.0”: “R” }, {_id:0})

取得Array中多個指定元素

如果要取得array中，同時包含R與G的document，直覺的寫法是 db.demo_1.find({color: [‘R’, ‘G’] }, {_id:0}) ，不過這個寫法意思是value完全等於['R', 'G']的document，所以不會找到['G', ‘R’]。

下一個選擇是使用 $or ， db.demo_1.find({$or: [{color: “G”}, {color: “R”}]}, {_id:0}) ，這個寫法的意思是，出現G、或是出現R，所以篩選出來的結果可能會出現非array的document，也會出現單一R的array。

正確的寫法，可以使用 $and ， db.demo_1.find({$and: [{color: “G”}, {color: “R”}]}, {_id:0}) ，會篩選出同時包含R、G的array，所以也會篩選出size大於2個array[‘R’, ‘G’, ‘B’]。如果使用Array Operator $all ，會更簡潔， db.demo_1.find({color: {$all: [“G”, “R”]}}, {_id:0}) 。

如果想要加上array size為2的條件， db.demo_1.find({$and: [{color: {$all: [“G”, “R”]}}, {color: {$size:2}}]}, {_id:0}) 。這樣就能篩選出僅有[‘R’ , ‘G’]與[‘G’ , ‘R’]的document。

Projection Operators - 輸出結果調整:矩陣調整

上面已經有使用到基本的用法，就是以在find method的第二個參數Projection，以 <field>: 0/ 1 的方式，調整一個field是否要呈現。例如 db.movies.findOne({}, {_id: 0, imdb_score: 1}) ，就是讓查詢結果僅顯示imdb_score且不顯示_id。

但如果需要的查詢結果是矩陣，就需要使用 $slice 進行調整。舉例而言，我們想僅僅想顯示field為actors的第一個元素，可以使用 {actors: {$slice: [0,1]}}

db.movies.find({}, {actors: {$slice: [0,1]}}).limit(1).pretty()

Cursor Methods - 修改query查詢結果

The Cursor is a MongoDB Collection of the document which is returned upon the find method execution. Cursor methods modify the way that the underlying query is executed.

find function查詢的結果稱為Cursor，如果想要對執行的結果進行其他處理，如統計數量、顯示、套用function等，就可以使用Cursor Methods。而其中有些方法是需要套用在Index中的，這裡先就一些簡單的使用方法說明，使用方式為 cursor.<method> ，以下說明幾個常用的method：

顯示：skip、limit

使用於想要限制查詢結果的數量，舉例而言，如果僅想顯示前10個結果，可以使用limit，可以寫成 db.movies.find({},{ _id:0, imdb_score: 1}).limit(10) ；如果想要顯示最後5個結果，可以使用skip，忽略前N個結果，因為document數量為95，可以寫成 db.movies.find({},{ _id:0, imdb_score: 1}).skip(95) 。

而如果想要看6到10的結果，就會是 db.movies.find({},{ _id:0, imdb_score: 1}).skip(5).limit(5) 。

計數：count、size

這兩個method都是用來統計find function結果document的數量，差異在於，count是統計的結果不會因為使用了skip或是limit有所改變，但是size僅統計使用了skip、limit的結果。

下面這個查詢式會回傳10個document，但是當使用count時，仍會回傳100；而使用size時，則會回傳10。

db.movies.find({},{ _id:0, imdb_score: 1}).limit(10)

轉換成 Array：toArray

非這個method是將查詢結果轉換為array，當轉換為array後，就可以再以insert轉存collection進行使用。

db.movies.find({},{ _id:0, imdb_score: 1}).limit(5).toArray()

函數映射：map

map的第一個參數可以放置javescript function，他可以將查詢結果，依據function功能進行映射。舉例而言，如果我想要針對查詢的結果運算，並同時顯示運算前後的結果。

我先建立一個function，他的功能是將imdb_score的數值開平方，並回傳一個array，內容是開平方前、後的數值結果。

temp_fun = function(u) {a = u.imdb_score**2; return [u.imdb_score, a]}

此時我可以將這個function套用到map之中

db.movies.find({},{ _id:0, imdb_score: 1}).limit(3).map(temp_function)

下面可以看到執行結果

Aggregation Pipeline Stages、Operators 語法結構

Aggregation pipeline顧名思義，就是可以拼接不同的查詢語句，以串聯的方式一個接著一個的method。而其中方法可以分為兩個層級，第一個層級為Stage，他是pipeline串接的基本單位。如以三個stage進行串接：

db.<collection_name>.aggregate(

[

{ <stage1> },

{ <stage2> },

{ <stage3> },

...

]

)

而第二個層級是Operators，將被使用在stage中，像是計算用的統計量 $max 、 $min ，或是條件式的方法 $cond 等等。他不會出現在第一個層級。以下就幾個常見的功能進行說明。

Aggregation Pipeline Stages：$match、$project

這兩個stage功能基本上就對應到find method中的Query 和 Projection，但這裡的project除了可以顯示之外，也可以創建一些新的field。

舉例而言，如果想篩選imdb_score大於8.5的結果，並且僅顯示imdb_score，在find method可以寫成：

db.movies.find(

{imdb_score: {$gte: 8.5}},

{_id:0, imdb_score:1}

)

而對應到aggregate的寫法，可以看到元素基本上是一樣的：

db.movies.aggregate(

[

{$match: {imdb_score: {$gte: 8.5}}},

{$project: {_id:0, imdb_score:1}}

]

)

如果要篩選imdb_score大於8.5的結果、且小於9的結果

find的寫法，用 $and 串接：

db.movies.find(

{$and: [{imdb_score: {$gte: 8.5}},

{imdb_score: {$lt: 9}}]},

{_id:0, imdb_score:1}

)

對應到aggregate，可以寫成：

db.movies.aggregate(

[

{$match: {$and: [{imdb_score: {$gte: 8.5}},

{imdb_score: {$lt: 9}}]}},

{$project: {_id:0, imdb_score:1}}

]

)

不過因為aggregate是pipeline，所以也可以寫成下面形式，讓的操作變化度更高：

db.movies.aggregate(

[

{$match: {imdb_score: {$gte: 8.5}}},

{$match: {imdb_score: {$lt: 9}}},

{$project: {_id:0, imdb_score:1}}

]

)

Aggregation Pipeline Stages：$group、$bucket、$bucketAuto

分組統計是aggregate與find method最重要的差異， $group 使我們可以根據指定field分組，而 $bucket 則可以將數值型的field進行分組統計。

$group

使用$group根據指定field值分組：寫法如下，其中 _id 是分組依據，而 <field1> 是自訂field， <accumulator1> 則是要進行的動作。

{$group:

{

_id: <expression>, // Group By Expression

<field1>: { <accumulator1> : <expression1> },

…

}

}

舉例而言，如果今天想統計不同城市中電影評分的總合，可以寫成：

db.movies.aggregate(

[

{$group:

{_id: "$country",

total: {$sum :"$imdb_score"}

}

}

]

)

如果今天想要根據兩個field進行分組，如根據城市與年份，可以寫成：

db.movies.aggregate(

[

{$group:

{_id: ["$country", "$year"],

total: {$sum :"$imdb_score"}

}

}

]

)

如果要計算多個統計量，則可以寫成依序自訂多個field：

db.movies.aggregate(

[

{$group:

{_id: "$country",

total: {$sum :"$imdb_score"},

avg: {$avg :"$imdb_score"},

max: {$max :"$imdb_score"},

std: {$stdDevSamp :"$imdb_score"},

first: {$first :"$imdb_score"},

count: {$sum :1}

}

}

]

).pretty()

$bucket

使用$bucket根據指定field值，自行切割分組：其語法如下，其中 groupBy 可以設定要分組的field； boundaries 則是可以將設定的field依據自訂的數值分組，例如field為1-100的實數，我可以每10設定一個組別； output 中設定的是自訂的field，一樣可以計算分組統計

{$bucket:

{

groupBy: <expression>, // Group By Expression

boundaries: [ <lowerbound1>, <lowerbound2>, ... ],

output: {

<field1>: { <accumulator1> : <expression1> },

…

}

}

}

舉例而言，如果我想統計特定年份區間的電影評分平均值、及電影數量，且分組方式為0-1999、2000-2004、2005-2009、2010-2999，則可以寫成：

db.movies.aggregate(

{

$bucket: {

groupBy: "$year",

boundaries: [ 0, 2000, 2005, 2010, 3000],

output: {

avg: {$avg :"$imdb_score"},

count: {$sum :1}

}

}

}).pretty()

$bucketAuto

與$bucket類似，但可以設定一個數字做為間隔來分組，寫法如下：

{$bucketAuto:

{

groupBy: <expression>, // Group By Expression

buckets: <number>,

output: {

<field1>: { <accumulator1> : <expression1> },

…

}

}

}

舉例而言，如果我想統計特定年份區間的電影評分平均值、及電影數量，且分組方式為每5個數字為一組，則可以寫成：

db.movies.aggregate(

{

$bucketAuto: {

groupBy: "$year",

buckets: 5,

output: {

avg: {$avg :"$imdb_score"},

count: {$sum :1}

}

}

}).pretty()

Aggregation Pipeline Operators：$cond, $switch in $project, $group

當需要以條件自訂field時，可以使用 $cond 與 $switch 兩個operator，前者是以if else建立，後者則可以給定多個條件。

$cond

$cond 是一個給定條件建立field的operator ，寫法如下：

{$project: <field1>: { $cond: { if: <boolean-expression>,

then: <true-case>,

else: <false-case> }

},

...

}

如果今天你想要根據一個條件進行分組，你可以先用 $project + $cond 建立field，再使用 $group。 舉例而言，如果我想依據國家創建一個新的field，分為是USA，或不是USA

db.movies.aggregate([

{

$project:{

area:{$cond: { //已$cond建立新的field，area

if: {$eq: ["$country", "USA"]},

then: "USA",

else: "Other"

}}, imdb_score: 1

}

},

{$group:

{_id: "$area", // 用新建立的field分組

total: {$sum :"$imdb_score"},

count: {$sum :1}

}

}

])

當然，因為 $cond 可以直接用於 $group 所以也可以直接在 $group 以 $cond 創立field。

db.movies.aggregate([

{

$group:{

_id:{$cond: {

if: {$eq: ["$country", "USA"]},

then: "USA",

else: "Other"

}},

total: {$sum :"$imdb_score"},

count: {$sum :1}

}

}])

$switch

$switch 可以依據多條件產生新的field，函數寫法如下， case 中擺的是條件式，而 then 中則是要指定的新value， default 則是非條件中的值。

{$project: <field1>: {$switch: {

branches: [

{ case: <expression>, then: <expression> },

{ case: <expression>, then: <expression> },

...

],

default: <expression>

}

}

舉例而言，如果我今天想對電影的年份建立一個新的類別，等於2007的為"Old"，介於2008到2015為"Middle"，超過2015則為"New"，非這些條件中的則為"Other"，可以寫成下式：

db.movies.aggregate([{

$project:{

_id:0,

year:1,

new_year_type: {$switch:{

branches:[

{

case: {$eq: ["$year", 2007]},

then: "Oooold"

},

{

case: {$and: [{$gt: ["$year", 2007]},

{$lte: ["$year", 2015]}]},

then: "Middle"

},

{

case: {$gt: ["$year", 2015]},

then: "New"

}

], default: "Other"}

}

}

}])

Aggregation Pipeline Stages：$out 輸出查詢結果

$out 的功能是將查詢結果另存到另一個collection，這個collection可以是既存的，也可以是新的。如下：

{ $out: { db: "<output-db>", coll: "<output-collection>" } }

舉例來說，如果我要將高於8.5分的電影結果，存到指定的collection “high_score_movie”之中：

db.high_score_movie.drop()

db.movies.aggregate(

[

{$match: {imdb_score: {$gte: 8.5}}},

{$out: "high_score_movie"}

]

)

db.high_score_movie.find().pretty()

結論

原本打算紀錄增刪改查的筆記，不過光是查的篇幅就是前一篇的數倍，並且許多重要的查詢功能內容並未提及，像是array在aggregate的操作、document的合併或是型態轉換等等，日後有機會再更新筆記。

因為篇幅長，鐵定有疏漏的 hahaha，再請不吝指教。