Mongo入门:数据去重之MapReduce,Aggregation的简单使用(另附相关网络资源)

先附上两个很好用也常见的链接:
- MongoDB 教程-RUNOOB
- The MongoDB 3.4 Manual
- 用通俗易懂的大白话讲解Map/Reduce原理(很通俗但是也很浅)。

BackGround

1. 需求:

查询某个月某医生有出诊计划的日期。(在mongo中去重能减轻传输网络负担以及程序的计算量)

Created with Raphaël 2.1.0MongoMongo程序程序结果结果传输计算

2. 数据:

  • 数据库中的测试数据如下:以病人为单位的计划信息
    • _id mongo 自动生成的文档唯一标识
    • _name 病人姓名
    • planId 某个医生的某个计划的标识(同一计划的起止时间相同)
    • drId 医生Id
    • 起止时间(mongo是+0000标准时间,所以换算成中国标准时间有8小时差)
{ 
    "_id" : ObjectId("586db879a8dab2b023fe2aa8"), 
    "name" : "陈三1", 
    "planId" : ObjectId("586db879a8dab2b023fe2aa7"), 
    "drId" : ObjectId("586daf5aa8dab2b023fe2a87"), 
    "timeStart" : ISODate("2017-10-21T16:00:00.000+0000"), 
    "TimeEnd" : ISODate("2017-11-29T16:00:00.000+0000")
}
{ 
    "_id" : ObjectId("586db879a8dab2b023fe2aaa"), 
    "name" : "陈三2", 
    "planId" : ObjectId("586db879a8dab2b023fe2aa9"), 
    "drId" : ObjectId("586daf5aa8dab2b023fe2a87"), 
    "timeStart" : ISODate("2017-10-21T16:00:00.000+0000"), 
    "TimeEnd" : ISODate("2017-11-29T16:00:00.000+0000")
}
{ 
    "_id" : ObjectId("586db879a8dab2b023fe2aac"), 
    "name" : "陈三3", 
    "planId" : ObjectId("586db879a8dab2b023fe2aab"), 
    "drId" : ObjectId("586daf5aa8dab2b023fe2a87"), 
    "timeStart" : ISODate("2017-10-21T16:00:00.000+0000"), 
    "TimeEnd" : ISODate("2017-11-29T16:00:00.000+0000")
}
{ 
    "_id" : ObjectId("586db879a8dab2b023fe2aae"), 
    "name" : "陈三4", 
    "planId" : ObjectId("586db879a8dab2b023fe2aad"), 
    "drId" : ObjectId("586daf5aa8dab2b023fe2a87"), 
    "timeStart" : ISODate("2017-10-21T16:00:00.000+0000"), 
    "TimeEnd" : ISODate("2017-11-29T16:00:00.000+0000")
}
  • Mongo测试数据插入语句:
    • MongoDB接入Javascrip风格语法,for,while,next,hasNext,forEach,toArray,findOne,limit,具体博文MongoDB高级查询详细。
    • 这里利用for循环循环插入4条数据。
    • objectId是唯一标识,参照菜鸟教程ObjectId。
for (var i = 1; i < 5; i++) db.test.insert({
  "name":"陈三"+i,
  "planId":ObjectId(),
  "drId":ObjectId("586daf5aa8dab2b023fe2a87"),
  "timeStart":ISODate("2017-10-21T16:00:00.000+0000"),
  "TimeEnd":ISODate("2017-11-29T16:00:00.000+0000")
  });

3. 程序流程

  • 输入输出
    • 输入数据:201710
    • 输出数据:20171022 20171023 …. 20171031
  • 流程:
    • 程序端:查询201710月 即 查询20171001~20171031,求出当月起止日与其他限制条件传给Mongo
    • Mongo服务端:查询限制条件下的集合返回
    • 程序端:将返回集合的起止日期转换为输出数据格式

Code

1. 输入数据的处理(golang):

    //将 201710 字符串 转换为 time类型 的 startYM(2017-10-01 00:00:00 +0800 CST),endYM(2017-10-31 00:00:00 +0800 CST)
    startYM, err := time.ParseInLocation("200601", "201710", time.Local)//time.Local是指你的字符串201710是你本地时区的201710,注意看转换结果:+0800 CST
    if err != nil {
        return nil, err
    }//错误处理的必要性见上一篇文
    endYM := startYM.AddDate(0, 1, -1)//+一月-一天得出最后一天

最后的限制条件的数据字段应是:

  • drId:ObjectId(“586daf5aa8dab2b023fe2a87”) //objectId可以说是一种特殊类型,要加上ObjectId的标识
  • startYM:2017-10-01 00:00:00 +0800 CST
  • endYM:2017-10-31 00:00:00 +0800 CST

    注:由于使用的是mgo的ORM,它会将time类型的+0800转换为+0000再去查询。 mgo的内容见mgo使用指南,mgo初探笔记。



    Mongo入门:数据去重之MapReduce,Aggregation的简单使用(另附相关网络资源)_第1张图片

    • 有这三种情况需要被查询到
    • 也就是不查询到endYM<计划开始时间,startYM>计划结束时间的数据
    • 最后限制条件也就是timeEnd>startYM,timeStart

2. Mongo查询:

1. 未去重

db.test.find({
  "drId":ObjectId("586daf5aa8dab2b023fe2a87"),
  "timeStart":{"$lte":ISODate("2017-10-31T16:00:00.000+0000")},
  "TimeEnd":{"$gte":ISODate("2017-10-01T16:00:00.000+0000")}
  })

2. Distinct

不同于其他的关系型数据库,Mongo中的distinct只能用于一个字段,也只能返回这个字段,见此篇文章的例子:MongoDB如何去除组合重复项。

3. Aggregation。

  • 他是Mongo为了方便使用,提供的一系列操作。能用Aggregation做的事情都能用MapReduce完成。
  • 这里有份很好的资料,【mongoDB高级篇①】聚集运算之group,aggregate。
  • 一份概念资料:MongoDB 聚合管道(Aggregation Pipeline)。
db.test.aggregate([
{$match:
    {"drId":ObjectId("586daf5aa8dab2b023fe2a87"),
    "timeStart":{"$lte":ISODate("2017-10-31T16:00:00.000+0000")},
    "timeEnd":{"$gte":ISODate("2017-10-01T16:00:00.000+0000")}
    }
},
{$group:{_id:"$planId",
        timeStart:{$first:"$timeStart"},
        timeEnd:{$first:"$timeEnd"},
        }},//需要注意,group是分组,如果要取这个分组每个集合的timeStart就用push,一条就用first,而不是意义不明的timeStart:"$timeStart",会报错

])

最后的结果是:

{ 
    "_id" : ObjectId("586db879a8dab2b023fe2aa7"), 
    "timeStart" : ISODate("2017-10-21T16:00:00.000+0000"), 
    "timeEnd" : ISODate("2017-11-29T16:00:00.000+0000")
}

4. MapReduce

  • 可以用Aggregation实现的都可以用MapReduce实现。网上很多类似资料出现好像原因是数据量过大,不得不去重。
  • Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。
  • MongoDB提供的Map-Reduce非常灵活,对于大规模数据分析也相当实用。
  • 很重要的一点是,必须先要了解什么是map/reduce函数,反正我踩坑了,没有js基础,这里推荐廖雪峰的js基础里的map/reduce讲解。
  • mongo中的map/reduce模型的基础知识。mongodb mapreduce使用总结,Mongodb MapReduce编程模型。

一种错误写法:

db.test.mapReduce(
function() {
    if (this.planId in planIds) { 
        return;
    }else{
        planIds[this.planId] = 1; 
        emit(this.planId,{"timeStart":this.timeStart,"timeEnd":this.timeEnd});      
         }
    },
function() {},              
{
      query: {"drId":ObjectId("586daf5aa8dab2b023fe2a87"),
            "timeStart":{"$lte":ISODate("2017-10-31T16:00:00.000+0000")},
            "timeEnd":{"$gte":ISODate("2017-10-01T16:00:00.000+0000")}
            },
      scope: {planIds:{}},//全局变量
      out: "result"
   }
).find()

结果:

{ 
    "_id" : ObjectId("586db879a8dab2b023fe2aa7"), 
    "value" : {
        "timeStart" : ISODate("2017-10-21T16:00:00.000+0000"), 
        "timeEnd" : ISODate("2017-11-29T16:00:00.000+0000")
    }
}

遇到的坑:

  • 首先是js语法,if in对数组而言,是遍历的角标,比如var a = ['A','B','C']; if ('A' in a) 是永假,因为’A’是和脚标(属性)进行的比对。如果是map,if in当然就是个map的key比较。所以这里用了map。具体见for of的讲解,有提到这个问题,,但是for of用于遍历。
  • 照我之前对这个代码的理解,其实是有问题的,虽然结果是对的(看起来):
    • map是分布式计算的,那么map并不能有效进行去重,只能保证其中一个分布式不重的,但是map函数结束之后传入reduce的数据还是有可能重合。
    • 但是根据结果,其实没有出问题,猜测因为emit本身就是一个map(key,value形式)。对结果又进行了去重,所以结果没问题(事实上是一个巧合),如果不加find()的结果如下:
{ 
    "result" : "result", 
    "timeMillis" : NumberInt(521), 
    "counts" : {
        "input" : NumberInt(4), 
        "emit" : NumberInt(1), //传递给reduce的数据本身只有一条,不存在被结果去重,具体emit的形式及错误原因见下文。
        "reduce" : NumberInt(0), 
        "output" : NumberInt(1)
    }, 
    "ok" : NumberInt(1)
}

另外的错误写法:

db.test.mapReduce(
function() {
    emit(this.planId,{"timeStart":this.timeStart,"timeEnd":this.timeEnd});      
    },
function() {},              
{
      query: {"drId":ObjectId("586daf5aa8dab2b023fe2a87"),
            "timeStart":{"$lte":ISODate("2017-10-31T16:00:00.000+0000")},
            "timeEnd":{"$gte":ISODate("2017-10-01T16:00:00.000+0000")}
            },
      out: "result"
   }
).find()

结果是错误的:

{ 
    "_id" : ObjectId("586db879a8dab2b023fe2aa7"), 
    "value" : null
}
  • Map函数返回的键值序列组合成{key,[value1,value2,value3,……]}传递给reduce,values的组合相当于廖雪峰reduce教材里说的[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)中的[x1,x2,x3,x4],并且顺序可能是f(f(x1,x2),f(x3,x4)),具体见《MongoDB权威指南》85页。
  • 不进行reduce处理直接输出就是null,参照map/reduce用来分类计数时的情况。其中的count就是用来在reduce中遍历递增:MongoDB学习笔记~管道中的分组实现group+distinct。
  • 那说明我们之前结果对了,可能是mongo实现机制的一个巧合(并没有分布计算),如果在将map数据聚合时,形成了value里是个集合,但是为什么输出null,目前还不知道怎么回事。尴尬。

正确写法:

db.test.mapReduce(
function() {
    emit(this.planId,{"timeStart":this.timeStart,"timeEnd":this.timeEnd});      
    },
function(key,values) {
    return {"timeStart":values[0].timeStart,"timeEnd":values[0].timeEnd}
},              
{
      query: {"drId":ObjectId("586daf5aa8dab2b023fe2a87"),
            "timeStart":{"$lte":ISODate("2017-10-31T16:00:00.000+0000")},
            "timeEnd":{"$gte":ISODate("2017-10-01T16:00:00.000+0000")}
            },
      out: "result"
   }
).find()

3. Mongo的返回值程序处理:

tempDays := make(map[string]bool)
    var timeStart, timeEnd time.Time
    for _, v := range snPlan {
        if v.Value.TimeEnd.Before(endYM) {
            timeEnd = v.Value.TimeEnd
        } else {
            timeEnd = endYM
        }//如果计划时间过长,要根据本月最后一天截取
        if v.Value.TimeStart.After(startYM) {
            timeStart = v.Value.TimeStart
        } else {
            timeStart = startYM
        }
        for i := timeStart; i.Before(timeEnd) || i.Equal(timeEnd); i = i.AddDate(0, 0, 1) {
            tempDays[i.Format(constants.DateFormatYYYYMMdd)] = true
        }
    }

这里能看出如果不去重对程序的影响很大,因为有双重循环+多个if语句。

你可能感兴趣的:(go基础,mongo)