mongoDB應(yīng)用篇-mongo聚合查詢

上篇我們學(xué)習(xí)了MongoDB中的一些特殊集合,如TTL集合與固定大小的集合,特殊的索引-文本索引,以及mongo的高級功能GridFS文件存儲功能的支持,本篇我們開始從數(shù)據(jù)分析的角度來學(xué)習(xí),MongoDB中多種查詢的數(shù)據(jù)聚合操作

如果我們在日常操作中,將部分數(shù)據(jù)存儲在了MongoDB中,但是有需求要求我們將存儲進去的文檔數(shù)據(jù),按照一定的條件進行查詢過濾,得到想要的結(jié)果便于二次利用,那么我們就可以嘗試使用MongoDB的聚合框架。

簡單的聚合查詢

前面我們在學(xué)習(xí)文檔查詢的過程中,也介紹過一些查詢的操作符,其中就有一部分是簡單的查詢聚合函數(shù),例如count、distinct、group等,如果是簡單的數(shù)據(jù)分析過濾,完全可以使用這些自帶的聚合函數(shù)以及查詢的操作符來完成文檔的過濾查詢操作

聚合框架

如果我們遇到了一些數(shù)據(jù)需要跨多個文本或者統(tǒng)計等操作,這個時候可能文檔自身也較為復(fù)雜,查詢操作符已經(jīng)無法滿足的時候,這個時候就需要使用MongoDB的聚合查詢框架了。

使用聚合框架可以對集合中的文檔進行變換和組合查詢,基本上我們使用的時候,都是使用多個構(gòu)件創(chuàng)建一個管道,用于對一連串的文檔進行處理。這里的構(gòu)件包括篩選(filter)、 投射(projecting)、分組(grouping)、排序(sorting)、限制(limiting)以及跳過(skipping)

aggregate函數(shù)

MongoDB中需要使用聚合操作,一般使用aggregate函數(shù)來完成多個聚合之間的連接,aggregate() 方法的基本語法格式如下 :

db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION)

現(xiàn)在假設(shè)我們有個集合articles,里面存儲了文章的集合,大致如下:

{
   _id: ObjectId(7df78ad8902c)
   title: 'MongoDB Overview', 
   description: 'MongoDB is no sql database',
   by_user: 'runoob.com',
   url: 'http://www.runoob.com',
   tags: ['mongodb', 'database', 'NoSQL'],
   likes: 100
},
{
   _id: ObjectId(7df78ad8902d)
   title: 'NoSQL Overview', 
   description: 'No sql database is very fast',
   by_user: 'runoob.com',
   url: 'http://www.runoob.com',
   tags: ['mongodb', 'database', 'NoSQL'],
   likes: 10
},
{
   _id: ObjectId(7df78ad8902e)
   title: 'Neo4j Overview', 
   description: 'Neo4j is no sql database',
   by_user: 'Neo4j',
   url: 'http://www.neo4j.com',
   tags: ['neo4j', 'database', 'NoSQL'],
   likes: 750
}

但這時我們需要查詢出來每一個作者寫的文章數(shù)量,需要使用aggregate()計算 ,大致如下:

db.articles.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}])

輸出的結(jié)果為:

{
   "result" : [
      {
         "_id" : "runoob.com",
         "num_tutorial" : 2
      },
      {
         "_id" : "Neo4j",
         "num_tutorial" : 1
      }
   ],
   "ok" : 1
}

通過這個簡單的案例我們就能輸出想要的數(shù)據(jù)和屬性名,大概分析一下剛剛的聚合查詢語句,則是按照group則是按照by_user字段進行分組,代表每個用戶一條數(shù)據(jù),而num_tutorial則是定義了數(shù)值類型計算的結(jié)果字段,$sum則是計算總和,相當于每個用戶出現(xiàn)一次,都會+1,最終計算出來的總和通過num_tutorial字段進行輸出

注:如果管道沒有給出預(yù)期的結(jié)果,就需要進行調(diào)試操作,調(diào)試的時候,可以嘗試先給一個管道操作符的條件,如果這個時候查詢出來的結(jié)果是我們想要的,那么我們需要再去指定第二個管道操作符,依次操作,最后就會定位到出了問題的操作符

管道操作符

前面我們提到聚合查詢會使用管道操作符,而每一個操作符就會接受一連串的文檔,對這些文檔進行一些類型轉(zhuǎn)換,最后將轉(zhuǎn)換以后的文檔結(jié)果傳遞給下一個管道操作符來執(zhí)行后續(xù)的操作,如果當前是最后一個管道操作符,那么則會顯示給用戶最后的文檔數(shù)據(jù)。不同的管道操作符是可以按照順序組合在一起使用,并且可以被重復(fù)執(zhí)行多次,例如我們可以先使用$match然后再去、group,最后再去執(zhí)行\(zhòng)match操作。

$match

match用于對文檔集合進行篩選,之后就可以在篩選得到的文檔子集上做聚合操作,\match管道操作符可以使用$gt、$lt、$in等操作符,進行過濾,不過需要注意的是不能在$match管道操作符中使用空間地理操作符。

在實際使用的過程中,盡可能的將match操作放在管道操作符的起始位置,這樣做的好處是可以快速的將不符合的文檔過濾掉,減少了文檔集的數(shù)量,其二是如果使用了\match操作符以后,再去投射或者執(zhí)行分組操作的話,是可以利用索引的。

$project

相比較一般的查詢操作而言,使用管道操作,尤其是其中的投射操作更加強大。我們可以在查詢文檔結(jié)束以后利用$project操作符從文檔中進行字段的提取,甚至于我們可以重命名字段,將部分字段映射成我們想要展示出去的字段,也可以對一部分字段進行一些有意義的處理。需要注意的是,$project操作符可以傳入兩個參數(shù),第一個是需要處理的屬性名稱,第二個則是0或者1,如果傳入1,則代表當前的屬性是需要顯示出來的,如果是0或者不寫,默認都是代表這個字段不需要顯示出來

當然第二個參數(shù)也可以是一個表達式或者查詢條件,滿足當前表達式的數(shù)據(jù)也可以進行顯示,接下來我們先準備一點數(shù)據(jù):

db.project.insertMany([
 { "_id" : 1, "item" : "abc", "price" : 10, "quantity" : 2, "date" : ISODate("2014-03-01T08:00:00Z") },
{ "_id" : 2, "item" : "jkl", "price" : 20, "quantity" : 1, "date" : ISODate("2014-03-01T09:00:00Z") },
{ "_id" : 3, "item" : "xyz", "price" : 5, "quantity" : 10, "date" : ISODate("2014-03-15T09:00:00Z") },
{ "_id" : 4, "item" : "xyz", "price" : 5, "quantity" : 20, "date" : ISODate("2014-04-04T11:21:39.736Z") },
{ "_id" : 5, "item" : "abc", "price" : 10, "quantity" : 10, "date" : ISODate("2014-04-04T21:23:13.331Z") }
])

接下來,我們來查詢,條件是item字段為abc,quantity要大于5,并且我們只要item和price字段的結(jié)果,其他都排除掉:

db.project.aggregate(
    [{"$match":{"item":"abc","quantity":{"$gt":5}}},{"$project":{"_id":0,"item":1,"price":1}}]
)

可以看到結(jié)果為:

{ 
    "item" : "abc", 
    "price" : 10.0
}

如果我們想要在原基礎(chǔ)上改變某個字段的名稱,例如將item改為item_code,可以利用$來完成,如下:

db.project.aggregate(
    [{"$match":{"item":"abc","quantity":{"$gt":5}}},{"$project":{"_id":0,"item_code":"$item","price":1}}]
)

可以看到我們指定的名稱item_code,而這個別名對應(yīng)的字段item使用$作為前綴標記,代表將item字段映射為item_code,可以看到結(jié)果:

{ 
    "price" : 10.0, 
    "item_code" : "abc"
}

簡單運算

我們在投影的時候,除了可以將某個字段映射成其他字段以外,還可以針對某個字段進行一些簡單的運算,最常見的就是四則運算,即

加法(add**)、減法(**subtract)、乘法(multipy**)、除法(**divide)、求模($mod) ,

除此之外,還支持對字段進行關(guān)系運算(大小比較("cmp"**)、等于(**"eq")、大于("gt"**)、大于等于(**"gte")、小于("le"**)、小于等于(**"lte")、不等于("ne"**)、判斷 null (**"ifNull") )、

邏輯運算(與("and"**)、或(**"or")、非 ("not"**) )以及**字符串操作**(連接(**"concat")、截?。?strong>"substr"**)、轉(zhuǎn)小寫(**"toLower") )等

我們基于上面的需求,假設(shè)每一個價格是按照元為單位,現(xiàn)在要求輸出W為單位,那么我們就需要對price進行除法運算,如下:

db.project.aggregate(
    [{"$match":{"item":"abc","quantity":{"$gt":5}}},{"$project":{"_id":0,"item_code":"$item","price":{"$divide":["$price",10000]}}}]
)

除此之外,我們也可以將計算完畢的price改名為priceW,即:

db.project.aggregate(
    [{"$match":{"item":"abc","quantity":{"$gt":5}}},{"$project":{"_id":0,"item_code":"$item","priceW":{"$divide":["$price",10000]}}}]
)

可以看到輸出的結(jié)果為:

{ 
    "item_code" : "abc", 
    "priceW" : 0.001
}

這時有一個需求,要求我們返回數(shù)據(jù)的同時還要yyyy-MM-dd格式的時間字符串,這個時候我們就需要對date字段進行時間函數(shù)和字符串混合處理了,如下:

db.project.aggregate(
    [{"$match":{"item":"abc","quantity":{"$gt":5}}},{"$project":{"_id":0,"item_code":"$item","price":{"$divide":["$price",10000]},"date_str":{$concat:[{$substr:["$date",0,4]},"-",{$substr:["$date",5,2]},"-",{$substr:["$date",8,2]} ]}}}]
)

這里需要注意的一點是,concat函數(shù)只能拼接字符串,如果我們使用{year:"date"}將當前的年份提取出來,是number類型的,這種是無法拼接的,因此我們可以使用\substr函數(shù)將date字段的結(jié)果截取成字符串即可實現(xiàn)拼接

$group

group操作可以將文檔依據(jù)特定字段的不同值進行分組,我們需要指定分組的字段,需要將其傳遞給\group的_id上,代表按照當前字段進行分組,例如,我們這里根據(jù)item進行分組:

db.project.aggregate([
 {$group:{"_id":"$item"}}
])
//結(jié)果為:
{ 
    "_id" : "jkl"
}
{ 
    "_id" : "xyz"
}
{ 
    "_id" : "abc"
}

分組操作符

在我們針對某個字段進行分組以后,我們可以針對每個分組進行一些操作符的使用,常見的例如:$sum、$avg、$min、$max、$first、$last

  • $sum:value

    $sum函數(shù)可以將我們用來分組的每一個分組的值進行累計,例如我們按照item分組,有三個結(jié)果,其中任何一個結(jié)果出現(xiàn)了幾次,就可以進行累加幾次,例如:

db.project.aggregate([
 {
        $group : {
            _id : "$item",
            count: { $sum : 1}
        }
    }
])
//輸出的結(jié)果為:
{ 
    "_id" : "jkl", 
    "count" : 1.0
}
// ----------------------------------------------
{ 
    "_id" : "xyz", 
    "count" : 2.0
}
// ----------------------------------------------
{ 
    "_id" : "abc", 
    "count" : 3.0
}
  • $avg : field

$avg操作符用來返回每一個分組內(nèi)的平均值

現(xiàn)在我們基于前面item的分組,我們想要算出來每個組內(nèi)的平均價格是多少,如下:

db.project.aggregate([
 {
        $group : {
            _id : "$item",
            count: { $sum : 1},
            avg:{$avg:"$price"}
        }
    }
])
//可見,結(jié)果為
{ 
    "_id" : "jkl", 
    "count" : 1.0, 
    "avg" : 20.0
}
..........
  • min/\max : field

$min$max操作符用于返回分組內(nèi)最大的值和最小的值

除了平均值以外,我們現(xiàn)在將最貴的和最便宜的價格也要列出來,這個時候就可以使用這兩個操作符了,如下:

db.project.aggregate([
 {
        $group : {
            _id : "$item",
            count: { $sum : 1},
            avg:{$avg:"$price"},
            max_price:{$max:"$price"},
            min_price:{$min:"$price"}
        }
    }
])
//結(jié)果
{ 
    "_id" : "jkl", 
    "count" : 1.0, 
    "avg" : 20.0, 
    "max_price" : 20.0, 
    "min_price" : 20.0
}
  • first/\last: filed

$first、$last則是可以獲取當前分組中第一個或者最后一個的某個字段的結(jié)果,如下:

db.project.aggregate([
 {
        $group : {
            _id : "$item",
            count: { $sum : 1},
            avg:{$avg:"$price"},
            max_price:{$max:"$price"},
            min_price:{$min:"$price"},
            first_price:{$first:"$price"},
            last_price:{$last:"$price"}
        }
    }
])
//結(jié)果
{ 
    "_id" : "jkl", 
    "count" : 1.0, 
    "avg" : 20.0, 
    "max_price" : 20.0, 
    "min_price" : 20.0, 
    "first_price" : 20.0, 
    "last_price" : 20.0
}

除此之外,我們還可以在分組的時候使用數(shù)組操作符,例如$addToSet可以判斷,當前數(shù)組如果不包含某個條件,就添加到當前數(shù)組中,$push則不管元素是否存在,都直接添加到數(shù)組中

注意:大部分管道操作符都是流式處理的,只要有新的文檔進入,就可以對新的文檔進行處理,但是$group代表必須收到全部文檔以后才可以進行分組操作,才會將結(jié)果傳遞給后續(xù)的管道操作符,這就意味著,如果當前mongo是存在分片的,會先在每個分片上執(zhí)行完畢以后,再把結(jié)果傳遞mongos進行統(tǒng)一的分組,剩下的管道操作符也不會在每個分片,而是mongos上執(zhí)行了

$unwind

如果我們現(xiàn)在遇到一些文檔比較復(fù)雜,比如存在內(nèi)嵌文檔的存在,某個屬性里面嵌套了一個數(shù)組,但是我們需要對內(nèi)嵌的數(shù)組文檔進行分析過濾等查詢處理,這個時候就可以使用$unwind操作符將每一個文檔中的嵌套數(shù)組文件拆分為一個個獨立的文檔便于進行后續(xù)的處理,例如我們需要將之前的set集合中關(guān)于請求的url以及ip的信息拆分出來,原始的格式如下:

{ 
    "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"), 
    "url" : "www.baidu.com", 
    "count" : 7.0, 
    "update_time" : "2020-08-13 12:00:00", 
    "ip_array" : [
        {
            "ip" : "192.168.1.3"
        }, 
        {
            "ip" : "192.168.1.4"
        }
    ]
}

我們可以使用命令進行拆分,如下:

db.set.aggregate([
{$project:{"url":1,"ip_array":1}},
{$unwind:"$ip_array"},
{$project:{"url":1,"ip":"$ip_array.ip"}}
])

結(jié)果為:

{ 
    "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"), 
    "url" : "www.baidu.com", 
    "ip" : "192.168.1.3"
},
{ 
    "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"), 
    "url" : "www.baidu.com", 
    "ip" : "192.168.1.4"
}

可以看到數(shù)據(jù)則是按照每一條信息的方式展示出來了,方便后續(xù)的計算以及輸出,但是需要注意的一點是,這種方式,如果該文檔中沒有拆分的字段,或者是空數(shù)組,默認會直接排除,如果我們需要空數(shù)組等也輸出計算出來,則可以指定preserveNullAndEmptyArrays參數(shù),設(shè)置為true,則代表空數(shù)組或者不存在的文檔也要拆分輸出出來,即:

db.set.aggregate([
{$project:{"url":1,"ip_array":1}},
{$unwind:{"path":"$ip_array","preserveNullAndEmptyArrays":true}},
{$project:{"url":1,"ip":"$ip_array.ip"}}
])
$sort

我們可以在管道查詢的過程中,按照某個屬性值或者多個屬性的結(jié)果進行順序排序,排序的方式與普通查詢操作符中的sort操作符表現(xiàn)一致,與其他管道操作符一樣,可以在任何階段使用,但是,需要注意的一點是,建議在管道操作符第一階段進行排序,因為此時的排序是可以觸發(fā)索引的,如果在后續(xù)階段進行排序,會消耗大量內(nèi)存,并且耗時會很久,尤其是在有$group的情況下,如果放在$group操作符后面,會發(fā)現(xiàn)等到的時間很久,不僅僅是無法觸發(fā)索引的問題,還和$group操作符是等待所有數(shù)據(jù)完畢才會觸發(fā)的特性有關(guān),因此需要格外注意。

db.project.aggregate([
{$project:{"_id":0,"item":1,"price":1,"quantity":1}},
{$sort:{"price":1,"quantity":1}}
])

結(jié)果如下,按照我們想要的結(jié)果進行了排序:

{ 
    "item" : "xyz", 
    "price" : 5.0, 
    "quantity" : 10.0
},
{ 
    "item" : "xyz", 
    "price" : 5.0, 
    "quantity" : 20.0
}
limit / \skip

limit與我們之前學(xué)過的操作符作用一樣,用于根據(jù)管道查詢的結(jié)果集中,返回n條結(jié)果的管道操作符,我們基于剛剛的查詢,加上\limit,只返回前兩條數(shù)據(jù),如下:

db.project.aggregate([
{$project:{"_id":0,"item":1,"price":1,"quantity":1}},
{$sort:{"price":1,"quantity":1}},
{$limit:2}
])

結(jié)果如下:

{ 
    "item" : "xyz", 
    "price" : 5.0, 
    "quantity" : 10.0
},
{ 
    "item" : "xyz", 
    "price" : 5.0, 
    "quantity" : 20.0
}

除了limit之外,我們常見的管道操作符還有\(zhòng)skip,與之前的查詢操作符作用也是一樣的,用于在已經(jīng)查詢完畢的結(jié)果集中跳過前N條數(shù)據(jù)以后進行返回,我們將$skip加在剛剛的查詢后面,如下:

db.project.aggregate([
{$project:{"_id":0,"item":1,"price":1,"quantity":1}},
{$sort:{"price":1,"quantity":1}},
{$limit:2},
{$skip:2},
])

這個時候可以看到返回的結(jié)果為空,什么結(jié)果都沒有了,這是因為前一步管道已經(jīng)限制了僅僅返回2條,而接著我們又跳過了前兩條文檔,因此返回的結(jié)果為空,我們將順序調(diào)換一下,看看:

db.project.aggregate([
{$project:{"_id":0,"item":1,"price":1,"quantity":1}},
{$sort:{"price":1,"quantity":1}},
{$skip:2},
{$limit:2},
])

可以看到結(jié)果如下,與剛才的結(jié)果無異:

{ 
    "item" : "abc", 
    "price" : 10.0, 
    "quantity" : 2.0
},
{ 
    "item" : "abc", 
    "price" : 10.0, 
    "quantity" : 2.0
}
管道操作符使用總結(jié)

管道查詢操作符有很多,除了上面學(xué)習(xí)的常用的部分,還有幾十個,需要了解全部的可以參考官網(wǎng):

https://docs.mongodb.com/manual/reference/command/aggregate/

除此之外,我們在學(xué)習(xí)的過程中了解到,部分查詢操作符是可以觸發(fā)索引的,例如$project$group或者$unwind操作符,因此我們也建議如果可以的話,盡量先使用這類管道操作符進行數(shù)據(jù)過濾,可以有效減少數(shù)據(jù)集大小和數(shù)量,而且管道如果不是直接從原先的集合中使用數(shù)據(jù),那就無
法在篩選和排序中使用索引,例如我們先進行管道操作,再去將過濾好的數(shù)據(jù)進行$sort排序,會導(dǎo)致無法使用索引,效率大幅度下降,因此如果我們需要涉及到$sort操作的時候,如果可以盡可能在最開始就處理,這個時候可以使用索引,效率較高,然后再去進行管道查詢篩選與分組等其他操作,可以有效的提高查詢的效率。另外需要注意的一點是,在MongoDB中會對每一個管道查詢做限制,例如某一步管道查詢操作導(dǎo)致內(nèi)存占用超過20%,這個時候就會報錯,無法繼續(xù)使用管道,因為mongoDB本身每次最大是16Mb的數(shù)據(jù)量,為了盡可能避免或者減少這種問題,建議可以考慮盡可能的使用$match操作符過濾無用數(shù)據(jù),減少數(shù)據(jù)總大小。同時也因為管道查詢是多步執(zhí)行,例如$group則是等待所有數(shù)據(jù)完畢才會執(zhí)行,因此可能會導(dǎo)致整體執(zhí)行時間較久,也因為這樣,才不建議在較高的實時查詢需求上使用管道和查詢,而是在設(shè)計的時候盡可能直接使用查詢操作符進行數(shù)據(jù)查詢,觸發(fā)更多的索引,更快的銷量查詢出來想要的結(jié)果。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容