MongoDB学习(五):聚合、管道与MapReduce

目录

一.聚合&管道

1.操作

2.例子

3.提高管道性能

二.Map/Reduce


聚合是MongoDB的高级查询框架,实际上在MySQL等关系数据库中,也有GROUP BY这样的类似功能。其主要作用是,从多个文档中提取、转换和整合数据,形成新的信息,可以用来发现文档间的一些关系,或者挖掘单个文档不具备的信息。例如,春节快到了,如果一家商店的店长想统计每月销售额、每种商品销售额、整年销售额,就必须以时间或商品ID作为分组条件进行统计。MongoDB提供了聚合和MapReduce两种工具,聚合要简单些。

一.聚合&管道

管道是计算机领域一个很普遍的概念,指的是对于一系列操作,前一个操作的结果通过管道输送给后一个操作,作为其输入。一个典型例子是linux的管道,通过配合grep、awk等工具,可以很方便的从命令行输出中提取出需要的信息。

1.操作

MongoDB有如下管道操作:

  • $project:可以用来修改文档的结构,类似于SQL的select
    • 可以用来对域进行重命名
    • 可以用来增加成员域
    • 可以删除指定域
  • $match:用于过滤出符合指定条件的文档,类似于SQL的where或having
  • $limit:用来限制管道流量
  • $skip:用来跳过指定数量的文档,返回剩余的文档
  • $unwind:将文档中某个数组类型字段进行拆分,为数组中的一个元素生成一个文档,类似于SQL的join
  • $group:将文档分组,类似于SQL的group by
    • 对于时间:
      • $dayOfYear: 返回该日期是这一年的第几天(全年 366 天)。
      •  $dayOfMonth: 返回该日期是这一个月的第几天(1到31)。
      •  $dayOfWeek: 返回的是这个周的星期几(1代表星期日,7代表星期六)。
      •  $year: 返回该日期的年份部分。
      •  $month: 返回该日期的月份部分( 1 到 12)。
      •  $week: 返回该日期是所在年的第几个星期( 0 到 53)。
      •  $hour: 返回该日期的小时部分。
      •  $minute: 返回该日期的分钟部分。
      •  $second: 返回该日期的秒部分(以0到59之间的数字形式返回日期的第二部分,但可以是60来计算闰秒)。
      •  $millisecond:返回该日期的毫秒部分( 0 到 999)。
      •  $dateToString: { $dateToString: { format,date} } 按照format的格式,将date转换为字符串
    • 对于其他字段:使用聚合操作符
      • $sum:计算总和
      • $avg:计算平均值
      • $min:获取某个字段的最小值
      • $max:获取某个字段的最大值
      • $push:将结果插入到一个数组中
      • $addToSet:将结果插入到Set中(即不存在重复文档)
      • $first:按照指定字段排序,获取第一个文档
      • $last:按照指定字段排序,获取最后一个文档
  • $sort:对输入的文档进行排序
  • $geoNear:输出接近某一地理位置的GEO文档
  • $out:把管道内的文档写入集合
  • $redact:控制对某些数据的访问

聚合管道的使用形式为:

db.collection_name.aggregate(pipeline,options)

有如下选项:

  • explain:打印执行计划,布尔值
  • allowDiskUse:使用磁盘暂存中间结果,布尔值,主要是为了解决中间数据太大(100MB以上)导致的报错
  • cursor:用于逐个获取管道结果,避免超出16MB限制

2.例子

下面举一些例子,先展示下user集合的数据,一共6条:

> db.user.find()
{ "_id" : ObjectId("5c3eef6d7da85af675c7c107"), "name" : "zhangsan", "sex" : "man", "age" : 21, "hobby" : "programming" }
{ "_id" : ObjectId("5c3eefee7da85af675c7c108"), "name" : "lisi", "sex" : "woman", "age" : 16, "hobby" : "music" }
{ "_id" : ObjectId("5c3ef0037da85af675c7c109"), "name" : "wangwu", "sex" : "man", "age" : 18, "hobby" : "read book" }
{ "_id" : ObjectId("5c3f1885cce0b679769390fa"), "name" : "lucy", "age" : 22, "hobby" : "movie", "sex" : "woman" }
{ "_id" : ObjectId("5c3f1bbfcce0b67976939109"), "name" : "tom", "age" : 2 }
{ "_id" : ObjectId("5c3f22a87da85af675c7c10a"), "name" : 10 }

1)首先统计每种性别的数量:

> db.user.aggregate({$group:{_id:'$sex',count:{$sum:1}}})
{ "_id" : null, "count" : 2 }
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }

_id就是用来指定分组依据,这里是sex域,注意需要带上美元符号$对于集合内每一个文档,如果sex字段相同,则分入同一组,反之亦然;$sum操作符的值(即1)代表每个组中每有一个文档,统计结果的"count"字段的值就加1。这个查询可以翻译成:select count(*) from user group by sex。可以看到,对于不存在的字段,也会作为null值参与统计。

2)假如只想统计有sex字段的文档,就可以用上$match操作符:

> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} })
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }

可以看到,$match的用法和find()函数很像。另外,当aggregate方法中存在多个操作时(即管道有多个环节),按照从左到右顺序执行。

3)MongoDB为aggregate提供了一个forEach()方法,可接受一个JavaScript函数,并继续处理管道内的数据:

> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} }).forEach(function(doc){if(doc.count%2 == 0) db.result.insert(doc);})
> db.result.find()
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }

这里使用的函数是,如果管道内的结果的count字段能整除2,则插入到result集合中,由于两个结果的count都是2,均满足条件,因此都插入了。

4)上例和以下语句作用相同:

> db.result.drop()
true
> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} },{$match:{count:{$mod:[2,0]}}},{$out:"result"})
> db.result.find()
{ "_id" : "woman", "count" : 2 }
{ "_id" : "man", "count" : 2 }

在这个管道中,首先过滤了没有sex域的文档,然后根据sex域的值进行分组,之后取count字段的值可以整除2的分组结果写入result集合。

5)如果不想看到_id字段,就可以用上$project了:

> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} },{$match:{count:{$mod:[2,0]}}},{$project:{_id:0}})
{ "count" : 2 }
{ "count" : 2 }

6)$project还可以用来改变字段名:

> db.user.aggregate({$match:{sex:{$exists:1}}},{$group:{_id:'$sex',count:{$sum:1}} },{$match:{count:{$mod:[2,0]}}},{$project:{"性别":"$_id","人数":"$count"}})
{ "_id" : "woman", "性别" : "woman", "人数" : 2 }
{ "_id" : "man", "性别" : "man", "人数" : 2 }

显然,_id字段不能被改名,对其使用$project相当于把该域的值复制一份再改名

由于$project具有投影的能力,因此也可以使用一些操作符,来实现诸如大小写转换、数学运算等操作:

  • 字符串类:
    • $concat:将给定的若干字符串连接为一个字符串
    • $strcasecmp:字符串比较(大小写不敏感)
    • $sunstr:对给定字符串进行裁剪
    • $toLower、$toUpper:全小写/全大写
  • 数字类:
    • $add、$substract、$multiply、$divide:加减乘除
    • $mod:整除
  • 时间类:请参照 “1.操作 - $group - 对于时间”
  • 逻辑操作:
    • $and:逻辑和,$not:逻辑非,$or:逻辑或
    • $eq、$gt、$gte、$lt、$lte、$ne:大小比较,返回布尔值
    • $cmp:大小比较,返回数值,0代表相等,1代表大于,-1代表小于
    • $cond:条件逻辑,即if...then...else
    • $ifNull:将空值替换为特定值
  • Set操作:
    • $setEquals:比较两个集合是否相同(包含相同元素)
    • $setIntersection:返回一个包含两个集合交集的数组
    • $setDifference:返回一个包含两个集合差集的数组
    • $setUnion:返回一个包含两个集合并集的数组
    • $setIsSubset:检查给定的第二个集合是否是第一个集合的子集
    • $anyElementTrue、$allElementsTrue:检查集合是否 有一个/全部 元素为true
  • 其它:
    • $meta:返回一些全文搜索的相关信息
    • $size:返回数组的元素数量
    • $map:对每个数组元素都应用给定的操作
    • $let:定义变量
    • $literal:返回表达式的值,而不计算它

3.提高管道性能

管道虽然好用,但是以下因素会对其性能产生影响:

  • 文档数量和文档的体积:应当尽可能小
  • 索引:可以大大加速管道操作,不过只能用于$match和$sort
  • 分片:如果集合进行了分片,那么只有$match和$project会在各个分片上运行,其他操作只会在主分片运行,一旦使用过其他操作,那么在此之后,管道只会在主分片上运行

例如:对$match和$group分别explain如下(需要先建立索引)

$match:

> db.user.aggregate([{$match:{sex:{$exists:1}}}],{explain:true})
{
        ……
        "indexName" : "sex_1",
        ……
}

可以看到,这里使用了索引

$group:

> db.user.aggregate([{$group:{_id:'$sex',count:{$sum:1}}}],{explain:true})
{
        "stages" : [
                {
                        "$cursor" : {
                                "query" : {

                                },
                                "fields" : {
                                        "sex" : 1,
                                        "_id" : 0
                                },
                                "queryPlanner" : {
                                        "plannerVersion" : 1,
                                        "namespace" : "test.user",
                                        "indexFilterSet" : false,
                                        "parsedQuery" : {

                                        },
                                        "winningPlan" : {
                                                "stage" : "COLLSCAN",
                                                "direction" : "forward"
                                        },
                                        "rejectedPlans" : [ ]
                                }
                        }
                },
                {
                        "$group" : {
                                "_id" : "$sex",
                                "count" : {
                                        "$sum" : {
                                                "$const" : 1
                                        }
                                }
                        }
                }
        ],
        "ok" : 1
}

并没有使用索引。在数据量小、管道短的情况下,感觉上没有什么差距,不过一旦任务复杂起来,差距就会很明显了。因此,在使用管道前,最好对相关域建立索引,或者建立专用的索引域。

二.Map/Reduce

MapReduce是MongoDB另一个数据处理工具,来源是Google的论文,主要思想是 分治-聚合 ,即将大的任务分割为小的任务并行处理,然后将结果聚合在一起。主要用在分布式、大数据条件下。

函数原型为:

db.collection_name.mapReduce(map,reduce,option)

map、reduce是两个函数。map函数生成键值对序列,使用emit返回,作为 reduce 函数参数;reduce函数将key-values变成key-value,也就是把values数组变成一个单一的值value。

option:

  • out 统计结果存放集合 (不指定则使用临时集合,在客户端断开后自动删除)。
  • query 一个筛选条件,只有满足条件的文档才会调用map函数。(query。limit,sort可以随意组合)
  • sort 和limit结合的sort排序参数(也是在发往map函数前给文档排序),可以优化分组机制
  • limit 发往map函数的文档数量的上限(要是没有limit,单独使用sort的用处不大)

示例如下:

> map = function(){emit(this.sex,1)}
function (){emit(this.sex,1)}
> reduce = function(k,v){return Array.sum(v)}
function (k,v){return Array.sum(v)}
> db.user.mapReduce(map,reduce,{query:{sex:{$exists:1}},out:"result"})
{
        "result" : "result",
        "timeMillis" : 115,
        "counts" : {
                "input" : 4,
                "emit" : 4,
                "reduce" : 2,
                "output" : 2
        },
        "ok" : 1
}
> db.result.find()
{ "_id" : "man", "value" : 2 }
{ "_id" : "woman", "value" : 2 }

效果和聚合一节中第二个例子一样

你可能感兴趣的:(MongoDB,mongodb,管道,聚合,aggregate,mapreduce)