mongodb 聚合管道

紅燒鯉魚發表於2018-09-24

管道概念

先介紹管道的概念,在POSIX多執行緒的使用方式中,定義了一種重要的pipeline方式,成為“流水線”或“管道”,這種方式使得資料被一組執行緒順序執行,其流程如下:

mongodb 聚合管道
以物件導向的思想去理解,整個流水線,可以理解為一個資料傳輸的管道;該管道中的每一個工作執行緒,可以理解為一個整個流水線的一個工作階段stage,這些工作執行緒之間的合作是一環扣一環的。靠輸入口越近的工作執行緒,是時序較早的工作階段stage,它的工作成果會影響下一個工作執行緒階段(stage)的工作結果,即下個階段依賴於上一個階段的輸出,上一個階段的輸出成為本階段的輸入。這也是pipeline的一個共有特點。

mongodb中的管道

mongodb在2.2版本中引入了聚合框架(aggregate framework)的新功能,它是聚合的新框架,其概念類似於資料處理的管道,每個文件經過一個由多個節點組成的管道,每個節點相當於流水線中的一個stage,有自己的功能(分組,過濾等),文件經過管道處理後,最後輸出相應的結果,管道的基本功能有兩個:

  1. 對文件進行“過濾”,篩選出合適的文件
  2. 對文件進行“變換”,改變文件的輸出形式

其他的一些功能還包括按照某個指定的欄位分組和排序等。而且在每個階段還可以使用表示式操作符計算平均值和拼接字串等相關操作。管道提供了一個MapReduce 的替代方案,MapReduce使用相對來說比較複雜,而管道的擁有固定的介面(操作符表達),使用比較簡單,對於大多數的聚合任務管道一般來說是首選方法。

mongodb中的聚合(aggregate)主要用於簡單的資料處理(平均值,求和等),並返回計算後的資料結果,類似於sql中的內嵌函式(count()等) 在mongodb官網給出了聚合框架的應用例項:

mongodb 聚合管道
可以看到,一個聚合管道中包含多個stage,每個stage都是對資料的一次處理,mongodb中的聚合採用aggregate()方法,語法如下: >db.COLLECTION_NAME.aggregate(pipeline,options) 其中pipeline為一個array,語法為: [{<stage1>,<stage2>,<stage3>,...}] 其中每個的語法為: {$管道操作符:{ 管道表示式 } }

那麼下面列舉一些較常見的管道操作符以及他們的作用,後文還會繼續給出例項:

操作符 描述 語法
$project 資料投影,主要用於重新命名,增加,刪除欄位 db.article.aggregate({ $project : {title : 1 ,author : 1 ,}});
$match 過濾,篩選符合條件的文件,作為下一階段輸入 db.articles.aggregate( [{ $match : { score : { $gt : 70, $lte : 90 } } },{ $group: { _id: null, count: { $sum: 1 } } }] );
$limit 限制經過管道的文件數量 db.article.aggregate({ $limit : 5 });
$skip 待操作集合處理前跳過部分文件 db.article.aggregate({ $skip : 5 });
$unwind 將陣列拆分成獨立欄位 db.article.aggregate({$project:{author:1,title:1,tags:1}},{$unwind:"$tags"})
$group 對資料進行分組 db.article.aggregate({ $group : {_id : "$author",docsPerAuthor : { $sum : 1 },viewsPerAuthor : { $sum : "$pageViews" }}});
$sort 對文件按照指定欄位排序 db.users.aggregate( { $sort : { age : -1, posts: 1 } });
$sample 隨機選擇從其輸入指定數量的文件。 { $sample: { size: <positive integer> } }
$out 必須為pipeline最後一個階段管道,因為是將最後計算結果寫入到指定的collection中
$indexStats 返回資料集合的每個索引的使用情況 { $indexStats: { } }

更多的管道操作符參考mongodb官方文件,官方文件寫的更為詳細,但是對語法的描寫較少 docs.mongoing.com/manual-zh/m…

管道操作符示例

下面針對常用管道操作符舉一些例子:

先載入資料:

use test1

db.mycol.remove({})

document1=({name:'dogOne',age:1,tags:['animal','dog'],type:'dog',money:[{min:100},{norm:200},{big:300}]});

document2=({name:'catOne',age:3,tags:['animal','cat'],type:'cat',money:[{min:50},{norm:100},{big:200}]});

document3=({name:'catTwo',age:2,tags:['animal','cat'],type:'cat',money:[{min:20},{norm:50},{big:100}]});

document4=({name:'dogTwo',age:5,tags:['animal','dog'],type:'dog',money:[{min:300},{norm:500},{big:700}]});

document5=({name:'appleOne',age:0,tags:['fruit','apple'],type:'apple',money:[{min:10},{norm:12},{big:13}]});

document6=({name:'appleTwo',age:0,tags:['fruit','apple'],type:'apple',money:[{min:10},{norm:12},{big:13}]});

document7=({name:'pineapple',age:0,tags:['fruit','pineapple'],type:'pineapple',money:[{min:8},{norm:9},{big:10}]});

db.mycol.insert(document1)

db.mycol.insert(document2)

db.mycol.insert(document3)

db.mycol.insert(document4)

db.mycol.insert(document5)

db.mycol.insert(document6)

db.mycol.insert(document7)
複製程式碼

下面是執行結果:

/* 1 */

{

    "_id" : ObjectId("59187984f322c585a98664e2"),

    "name" : "dogOne",

    "age" : 1.0,

    "tags" : [ 

        "animal", 

        "dog"

    ],

    "type" : "dog",

    "money" : [ 

        {

            "min" : 100.0

        }, 

        {

            "norm" : 200.0

        }, 

        {

            "big" : 300.0

        }

    ]

}



/* 2 */

{

    "_id" : ObjectId("59187984f322c585a98664e3"),

    "name" : "catOne",

    "age" : 3.0,

    "tags" : [ 

        "animal", 

        "cat"

    ],

    "type" : "cat",

    "money" : [ 

        {

            "min" : 50.0

        }, 

        {

            "norm" : 100.0

        }, 

        {

            "big" : 200.0

        }

    ]

}



/* 3 */

{

    "_id" : ObjectId("59187984f322c585a98664e4"),

    "name" : "catTwo",

    "age" : 2.0,

    "tags" : [ 

        "animal", 

        "cat"

    ],

    "type" : "cat",

    "money" : [ 

        {

            "min" : 20.0

        }, 

        {

            "norm" : 50.0

        }, 

        {

            "big" : 100.0

        }

    ]

}



/* 4 */

{

    "_id" : ObjectId("59187984f322c585a98664e5"),

    "name" : "dogTwo",

    "age" : 5.0,

    "tags" : [ 

        "animal", 

        "dog"

    ],

    "type" : "dog",

    "money" : [ 

        {

            "min" : 300.0

        }, 

        {

            "norm" : 500.0

        }, 

        {

            "big" : 700.0

        }

    ]

}



/* 5 */

{

    "_id" : ObjectId("59187984f322c585a98664e6"),

    "name" : "appleOne",

    "age" : 0.0,

    "tags" : [ 

        "fruit", 

        "apple"

    ],

    "type" : "apple",

    "money" : [ 

        {

            "min" : 10.0

        }, 

        {

            "norm" : 12.0

        }, 

        {

            "big" : 13.0

        }

    ]

}



/* 6 */

{

    "_id" : ObjectId("59187984f322c585a98664e7"),

    "name" : "appleTwo",

    "age" : 0.0,

    "tags" : [ 

        "fruit", 

        "apple"

    ],

    "type" : "apple",

    "money" : [ 

        {

            "min" : 10.0

        }, 

        {

            "norm" : 12.0

        }, 

        {

            "big" : 13.0

        }

    ]

}



/* 7 */

{

    "_id" : ObjectId("59187984f322c585a98664e8"),

    "name" : "pineapple",

    "age" : 0.0,

    "tags" : [ 

        "fruit", 

        "pineapple"

    ],

    "type" : "pineapple",

    "money" : [ 

        {

            "min" : 8.0

        }, 

        {

            "norm" : 9.0

        }, 

        {

            "big" : 10.0

        }

    ]

}
複製程式碼

1.$project操作符與$match操作符

$project管道操作符用於修改流中的文件,$match管道操作符用於對流中的文件進行過濾,僅允許符合條件的文件進入下一個階段,過濾操作不會修改文件。$match操作使用mongodb標準的查詢條件,對於每一個輸入文件,如果符合條件,則輸出這個文件,否則丟棄該文件。由於aggregate管道對於記憶體的限制,在處理大檔案的時候,最好先用match操作符進行篩選,減少記憶體佔用。

假定我們想提取money中min為100的文件,並且只輸出名稱和money陣列中的min那一項,用$project$match操作符可以很好的實現

use test1
db.mycol.aggregate(
    {$match:{'money.min':100}},
    {$project:{_id:0,name:'$name',minprice:'$money.min'}}
    )
複製程式碼

輸出結果為:

/* 1 */

{

    "name" : "dogOne",

    "minprice" : [ 

        100.0

    ]

}
複製程式碼

可以發現,在project操作符後,文件中的欄位被改變了。 也要注意到,對於陣列中物件的引用,需要採用 '$money.min'形式 注意:

  1. 不能在$match操作符中使用$where表示式操作符。
  2. $match儘量出現在管道的前面,這樣可以提早過濾文件,加快聚合速度。
  3. 如果$match出現在最前面的話,可以使用索引來加快查詢。

2.$limit $skip操作符

$limit$skip操作符是用於限制與跳過相應文件,與find中的limit與skip方法效果相同。 假定我們想提取money中min小於100的文件,並且限制3個文件,跳過一個文件再顯示 指令碼為

use test1

db.mycol.aggregate(

    {$match:{'money.min':{$lt:100}}},

    {$limit:3},

    {$skip:1},

    {$project:{_id:0,name:'$name',minprice:'$money.min'}}

    )
複製程式碼

結果為:

/* 1 */

{

    "name" : "catTwo",

    "minprice" : [ 

        20.0

    ]

}



/* 2 */

{

    "name" : "appleOne",

    "minprice" : [ 

        10.0

    ]

}
複製程式碼

可以發現結果滿足我們的需求

3.$group操作符

$group操作符用來對資料進行分組。 $group的時候必須要指定一個_id域,同時也可以包含一些算術型別的表示式操作符 比如我們要通過type型別來對資料進行分類,並且同時統計他們的年齡age總和, 指令碼為:

use test1

db.mycol.aggregate(

    {$group:{_id:'$type',sumage:{$sum:'$age'}}}

)
複製程式碼

結果為:

/* 1 */

{

    "_id" : "pineapple",

    "sumage" : 0.0

}



/* 2 */

{

    "_id" : "cat",

    "sumage" : 5.0

}



/* 3 */

{

    "_id" : "apple",

    "sumage" : 0.0

}



/* 4 */

{

    "_id" : "dog",

    "sumage" : 6.0

}
複製程式碼

可以看到資料按照 貓,狗,蘋果,菠蘿進行了分類,並且年齡相加了。 注意:

  1. $group的輸出是無序的。
  2. $group操作目前是在記憶體中進行的,所以不能用它來對大量個數的文件進行分組。
  3. 必須指定 _id 的域

4. $sort操作符

sort操作符用來對資料進行排序,同樣1代表升序,-1代表降序 假定我們按照年齡對資料進行排序,為了減少輸出行數,我們用上分組與skip 指令碼為:

use test1

db.mycol.aggregate(

    {$group:{_id:'$type',sumage:{$sum:'$age'}}},

    {$skip:1},

    {$sort:{sumage:1}}

)
複製程式碼

結果為:

/* 1 */

{

    "_id" : "apple",

    "sumage" : 0.0

}



/* 2 */

{

    "_id" : "cat",

    "sumage" : 5.0

}



/* 3 */

{

    "_id" : "dog",

    "sumage" : 6.0

}
複製程式碼

注意:

  1. 如果將$sort放到管道前面的話可以利用索引,提高效率
  2. MongoDB 對記憶體做了優化,在管道中如果$sort出現在$limit之前的話,$sort只會對前 $limit個文件進行操作,這樣在記憶體中也只會保留前$limit個文件,從而可以極大的節省記憶體
  3. $sort操作是在記憶體中進行的,如果其佔有的記憶體超過實體記憶體的10%,程式會產生錯誤

最後

其他不常用的操作符暫不說明。 需要額外注意操作符對記憶體的要求。

相關文章