V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
threedream
V2EX  ›  程序员

关于 MongoDB aggregate 和 mapReduce 的统计问题

  •  
  •   threedream · 2017-12-13 09:50:43 +08:00 · 3256 次点击
    这是一个创建于 2538 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近使用 mongoDB 存储了大量的用户登录记录,文档结构如下:

    {
      "_id" : ObjectId("5a1fde257756dca1c86f2d23"),
      "_class" : "com.aaa.LoginHistory",
      "uid" : "94989242",
      "ip" : "192.168.1.219",
      "imei" : "865736037366641",
      "epid" : "33189023-9472-4f30-81cf-8c7d13132aae5",
      "platform" : "Android",
      "timestamp" : "1512037117100",
      "address" : "中国,江苏省,南京市,建邺区"
    }
    
    • 现在有个需求,需要统计每一天( yyyy-MM-dd )的登录用户数( uid 对应用户,每天存在多次登陆记录,即 uid 可能重复)。
    • 由于技术菜,选择了比较 low 的方法实现:根据需要查询的时间间隔的时间,就是开始时间戳和结束时间戳,把它分成每天一个间隔,然后利用以下方法去统计:
    db.loginHistory.aggregate([
    	{$match:{"timestamp":{"$lte":"1513412162000","$gte":"1512548162000"}}},
        {$group:{_id:"$uid"}}
    ])
    

    因此出现了很大的问题:

    • uid 的重复,导致登录用户数量统计出现重复。
    • 如果需要查询一年以内的话,则需分成 365 个时间区不断得循环调用,导致响应时间超级慢。

    最后经过各种百度找到了一种实现:

    db.loginHistory.aggregate(
        [
            {
                $match:{
                    timestamp:{
                        $lte:"1513399696240",
                        $gte:"1513389696240"
                    }
                }
            },
            {
                $project:{
                    uid:1,
                    timestamp:1,
                    _id:0
                }
            },
            {
                $group:{
                    _id:{
                        uid:"$uid",
                        date:{
                            $dateToString:{
                                format:"%Y-%m-%d",
                                date:{
                                    $add:[new Date(0),parseInt("$timestamp")]
                                }
                            }
                        }
                    }
                }
            },
            {
                $group:{
                    _id:{
                        date:"$_id.date"
                    },
                    count:{"$sum":1}
                }
            }
        ]
    )
    

    但是问题又来了,执行报错:

    assert: command failed: {
        "ok" : 0,
        "errmsg" : "$dateToString is only defined on year 0-9999, tried to use year 292278994",
        "code" : 18537
    } : aggregate failed
    _getErrorWithCode@src/mongo/shell/utils.js:25:13
    doassert@src/mongo/shell/assert.js:13:14
    assert.commandWorked@src/mongo/shell/assert.js:287:5
    DBCollection.prototype.aggregate@src/mongo/shell/collection.js:1312:5
    

    百思不得其解,尝试性的把 parseInt("$timestamp")改成 parseInt("1513399696240"),执行得到结果:

    { "_id" : { "date" : "2017-12-16" }, "count" : 19 }
    

    经过 v2 上的大哥提建议说用 mapReduce,尝试着改成了 mapReduce,结果成功了,可是:

    db.loginHistory.mapReduce(
        function(){  
            var date = new Date(parseInt(this.timestamp));
            var dateKey = date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate();
            emit(dateKey,{uid:this.uid,count:1});  
        },    
        function(key, values){ 
            var result = 0;
            var json = {};
            for (var i = 0; i < values.length; i++) {
                if(json[values[i].uid] != 1) {
                    result += values[i].count;
                    json[values[i].uid] = 1;
                }
            }
            
            return {count:result};  
        },
        {
            query:{
                "timestamp":{
                    "$gte":"1512662400000","$lte":"1512921599999"
                },
                "epid":"a6c89023-9472-4f30-81cf-8c7dea62aae5",
                "platform":"ios"
            },
            out:"aaa"
        }
    ).find()
    

    109W 条数据,花了 11 秒,没错,就是 11 秒,就一台单机 mongoDB,我觉得用 mapReduce 有意义?

    线上大概数据已经超过 5 千万条,如果不用 aggregate 的话,30 秒超时时间都不够。。。

    10 条回复    2017-12-15 12:19:18 +08:00
    threedream
        1
    threedream  
    OP
       2017-12-13 09:51:20 +08:00
    求助求助,求助各位大佬。
    Had
        2
    Had  
       2017-12-13 10:22:17 +08:00
    不要重复查不就行了?
    单独拉个表,存储已经查询出来的记录,也就是你每天过 12 点跑一个任务,把前一天内的登陆用户查出来,然后做 unique 就行了呗,这样每天就查一天的数据量,负担也很小,也方便查
    完全没有必要天天重复跑已经确定了的数据啊
    a342191555
        3
    a342191555  
       2017-12-13 10:29:26 +08:00 via iPhone
    瞎说一种思路…用聚合管道。第一步,把每个 document project 成:{uid:.....,dayOfYear:x}的形式,第二步,group 操作,以 dayOfYear 为_id,把 uid 放到数组中,第三步,去重。
    a342191555
        4
    a342191555  
       2017-12-13 10:45:40 +08:00
    @a342191555 想了想,这样要遍历多次,改写成 Map-Reduce 试试:

    var map = function () {
    var key=Math.floor((parseInt(this.timestamp)-1483200000)/3600/24); // 计算当前时间是 2017 年第几天
    var value=this.uid;
    emit(key, value);
    };

    var reduce = function (key, values) {
    var uids={};
    for(var uid in values){
    uids[uid]=1;
    }

    var returnedValues=0; //去重
    for(var i in uids){
    returnedValues++;
    }
    return returnedValues;
    };
    jy02201949
        5
    jy02201949  
       2017-12-13 10:51:17 +08:00
    收藏了,坐看各位的解决思路,这个时候我开始怀念起了 ORACLE
    fds
        6
    fds  
       2017-12-13 11:04:55 +08:00
    同意楼上说的,把统计结果存起来,下次查就快了。
    另外反正就是遍历,直接写个程序 find 区间然后调用 cursor.next()遍历所有条目,自己排重即可。

    你说的代码里 date:{$add:[new Date(0),parseInt("$timestamp")]} 这段肯定有问题,parseInt 是个函数,你这么调用就是个字符串,不可能用数据库里的值代替的。改成 date:{$add:[new Date(0),"$timestamp"]} 应该可以。
    baiihcy
        7
    baiihcy  
       2017-12-13 11:09:11 +08:00
    是要把相同 uid 的不同 timestamp 查询出来吗?

    db.loginHistory.aggregate(
    [
    {
    $match:{
    timestamp:{
    $lte:"1513399696240",
    $gte:"1513389696240"
    }
    }
    },
    {
    $project:{
    uid:1,
    timestamp:1,
    _id:0
    }
    },
    {
    $group:{
    _id:{
    uid:"$uid"
    },
    timestamp:{
    $push:"$timestamp"
    }
    }
    }
    ]
    )
    rushpu
        8
    rushpu  
       2017-12-13 12:08:40 +08:00
    把你数据库的 timestamp 搞成 NumberLong 型的,
    然后
    aggregate([
    {
    "$group": {
    "_id": {
    "$dateToString": {
    "format": "%Y-%m-%d",
    "date": {
    "$add": [
    new Date(0),
    "$timestamp"
    ]
    }
    }
    },
    "count": { "$sum": 1 }
    }
    }
    ])
    threedream
        9
    threedream  
    OP
       2017-12-15 11:14:11 +08:00
    @a342191555 大哥,用你这个去重方法,我输出 uids,发现数据的 key 是有问题的,并没有实现去重。
    uids 数据如下:
    { "_id" : "2017-12-13", "value" : { "0" : 1, "1" : 1, "2" : 1, "3" : 1, "4" : 1, "5" : 1, "6" : 1, "7" : 1, "8" : 1, "9" : 1, "10" : 1, "11" : 1, "12" : 1, "13" : 1, "14" : 1, "15" : 1, "16" : 1, "17" : 1, "18" : 1, "19" : 1, "20" : 1, "21" : 1, "22" : 1, "23" : 1, "24" : 1, "25" : 1, "26" : 1, "27" : 1, "28" : 1, "29" : 1, "30" : 1, "31" : 1, "32" : 1, "33" : 1, "34" : 1, "35" : 1, "36" : 1, "37" : 1, "38" : 1, "39" : 1, "40" : 1, "41" : 1, "42" : 1, "43" : 1, "44" : 1, "45" : 1, "46" : 1, "47" : 1, "48" : 1, "49" : 1, "50" : 1, "51" : 1, "52" : 1, "53" : 1, "54" : 1, "55" : 1, "56" : 1, "57" : 1, "58" : 1, "59" : 1, "60" : 1, "61" : 1, "62" : 1, "63" : 1, "64" : 1, "65" : 1, "66" : 1, "67" : 1, "68" : 1, "69" : 1, "70" : 1, "71" : 1, "72" : 1, "73" : 1, "74" : 1, "75" : 1, "76" : 1, "77" : 1, "78" : 1, "79" : 1, "80" : 1, "81" : 1, "82" : 1, "83" : 1, "84" : 1, "85" : 1, "86" : 1, "87" : 1, "88" : 1, "89" : 1, "90" : 1, "91" : 1, "92" : 1, "93" : 1, "94" : 1, "95" : 1, "96" : 1, "97" : 1, "98" : 1 } }
    a342191555
        10
    a342191555  
       2017-12-15 12:19:18 +08:00 via iPhone
    @threedream 我写的那个 reduce 函数最后那段已经去重了呀。贴你的代码。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2596 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 15:31 · PVG 23:31 · LAX 07:31 · JFK 10:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.