• MongoDB学习笔记五:聚合


    『count』
    count是最简单的聚合工具,返回集合中的文档数量:
    > db.foo.count()
    0
    > db.foo.insert({"x" : 1})
    > db.foo.count()
    1
    也可以传递查询,Mongo则会计算查询结果的数量:
    > db.foo.insert({"x" : 2})
    > db.foo.count()
    2
    > db.foo.count({"x" : 1})
    1
    『distinct』
    distinct用来找出给定键的所有不同的值。使用时必须制定集合和键。
    > db.runCommand({"distinct" : "people", "key" : "age"})
    例如,假设有如下文档:
    {"name" : "Ada", "age" : 20}
    {"name" : "Fred", "age" : 35}
    {"name" : "Susan", "age" : 60}
    {"name" : "Andy", "age" : 35}
    如果对"age"使用distinct,会获得所有不同的年龄:
    > db.runCommand({"distinct" : "people", "key" : "age"})
    {"values" : [20, 35, 60], "ok" : 1}
    『group』
    group先选定分组所依据的键,而后MongoDB就会将集合依据选定键值的不同分成若干组。然后通过聚合每一组内的文档,产生一个结果文档。
    (这个group和SQL中的GROUP BY差不多。)
    假设现在有个站点要跟踪股票价格。从上午10点到下午4点每隔几分钟就更新一下某只股票的价格,并保存在MongoDB中。现在报表程序要获得近30天的收盘价。用group就可以很容易地办到。
    股价的集合中包含数以千计的如下形式的文档:
    {"day" : "2010/10/03", "time" : "10/3/2010 03:57:01 GMT-400", "price" : 4.23}
    {"day" : "2010/10/04", "time" : "10/4/2010 11:28:39 GMT-400", "price" : 4.27}
    {"day" : "2010/10/03", "time" : "10/3/2010 05:00:23 GMT-400", "price" : 4.10}
    {"day" : "2010/10/06", "time" : "10/6/2010 05:27:58 GMT-400", "price" : 4.30}
    {"day" : "2010/10/04", "time" : "10/4/2010 08:34:50 GMT-400", "price" : 4.01}
    想获得的结果就是每天最后的价格列表,就像这样:
    [
    {"day" : "2010/10/03", "time" : "10/3/2010 05:00:23 GMT-400", "price" : 4.10}
    {"day" : "2010/10/04", "time" : "10/4/2010 11:28:39 GMT-400", "price" : 4.27}
    {"day" : "2010/10/06", "time" : "10/6/2010 05:27:58 GMT-400", "price" : 4.30}
    ]
    先把集合按照天分组,然后在每一组里取包含最新时间戳的文档,将其放置到结果中就完成了。整个过程:

    > db.runCommnad({"group" : {
            "ns" : "stocks",
            "key" : "day",
            "initial" : {"time" : 0},
            "$reduce" : function(doc, prev) {
                if (doc.time > prev.time) {
                    prev.price = doc.price;
                    prev.time = doc.time;
                }
            }}})

    分解步骤如下:
    "ns" : "stocks"
    指定要进行分组的集合。
    "key" : "day"
    指定文档分组依据的键。这里就是"day"键,所有"day"值相同的文档被划分到一组。
    "initial" : {"time" : 0}
    每一组reduce函数调用的初试时间,会作为初始文档传递给后续过程。每一组的所有成员都会使用这个累加器,所以改变会保留住。
    "reduce" : function(doc, prev) { ... }
    每个文档都对应一次这个调用。系统会传递两个参数:当前文档和累加器文档(本组当前的结果)。本例中,想让reduce函数比较当前文档的时间和累加器文档的时间。如果当前文档的时间更近,则将累加器的日期和价格替换成当前文档的值。每一组都有一个独立的累加器,所以不用担心不同的日期使用同一个累加器。
    在问题一开始的描述中,就提到只要最近30天的股价。然而,这里迭代了整个集合,这就是为什么要添加"condition",因为这样就可以值处理满足条件的文档了。

    > db.runCommnad({"group" : {
            "ns" : "stocks",
            "key" : "day",
            "initial" : {"time" : 0},
            "$reduce" : function(doc, prev) {
                if (doc.time > prev.time) {
                    prev.price = doc.price;
                    prev.time = doc.time;
                }},
            "condition" : {"day" : {"$gt" : "2010/09/30"}}
            }})    

    最后就会返回由30个文档组成的数组,每个组一个文档。魅族还有分组依据的键(这里就是"day" : string)以及这组最终的prev值。如果有的文档没有依据的键,就都会被分到一组,相应的部分就会使用"day : null"这样的形式。在"condition"中加入"day" : {"$exists" : true}就可以去掉这组。
    使用完成器
    完成器(finalizer)用以精简从数据库传到用户的数据。
    例:博客,其中每篇文章都有多个标签(tag)。现在要找出每天最热点的标签。可以(再一次)按天分组,为每一个标签计数:

    > db.posts.group({
        "key" : {"tags" : true},
        "initial" : {"tags" : {}},
        "$reduce" : function(doc, prev) {
            for (i in doc.tags) {
                if (doc.tags[i] in prev.tags) {
                    prev.tags[doc.tags[i]]++;
                } else {
                    prev.tags[doc.tags[i]] = 1;
                }
            }
        }})

    结果会是这样:
    [
    {"day" : "2010/01/12", "tags" : {"nosql" : 4, "winter" : 10, "sledding" : 2}}
    {"day" : "2010/01/13", "tags" : {"soda" : 5, "php" : 2}}
    {"day" : "2010/01/14", "tags" : {"python" : 6, "winter" : 4, "nosql" : 15}}
    ]
    然后,使用finalizer过滤服务器传递给客户端过程中不需要的数据:

    > db.posts.group({
        "key" : {"tags" : true},
        "initial" : {"tags" : {}},
        "$reduce" : function(doc, prev) {
            for (i in doc.tags) {
                if (doc.tags[i] in prev.tags) {
                    prev.tags[doc.tags[i]]++;
                } else {
                    prev.tags[doc.tags[i]] = 1;
                }
            }, 
        "finalize" : function(prev) {
            var mostPopular = 0;
            for(i in prev.tags) {
                if(prev.tags[i] > mostPopular) {
                    prev.tag = i;
                    mostPopular = prev.tags[i]
                }
            }
            delete prev.tags
        }}})

    然后,服务器会返回希望的结果:
    [
    {"day" : "2010/01/12", "tag" : "winter"},
    {"day" : "2010/01/13", "tag" : "soda"},
    {"day" : "2010/01/14", "tag" : "nosql"}
    ]
    finalize嫩刚修改传递的参数也能返回新值。
    将函数作为键使用
    定义分组函数要用到"$keyf"键。
    例如,由于有很多作者,给文章分类时可能不规律的用了大小写,为了让"MongoDB"和"mongodb"分在同一个组,需要使用"$keyf"定义一个函数来确定文档分组所依据的键:
    > db.posts.group({"ns" : "posts", "$keyf" : function(x) { return x.category.toLowerCase(); }, "initializer" : ... })
    有了"$keyf"就能依据各种复杂的条件进行分组了。
    『MapReduce』
    MapReduce:
    ①映射(map):将操作映射到集合中的每个文档。
    ②洗牌(shuffle):按照键分组,并将产生的键值组成列表放到对应的键中。
    ③化简(reduce):把列表中的值简化成一个单值。这个值被返回,然后接着进行洗牌,直到每个键的列表只有一个值为止,这个值就是最后结果。
    使用MapReduce的代价就是速度:group不是很快,MapReduce更慢,绝对不要用在“实时”环境中。
    【MapReduce例1:找出集合中的所有键】
    MongoDB没有模式,所以并不知晓每个文档有多少个键。通常找到集合的所有键的最好方式就是用MapReduce。在本例中,还会记录每个键出现了多少次。
    在映射(map)环节,想得到文档中的每个键。map函数使用emit“返回”要处理的值。emit会给MapReduce一个键(类似于前面group多使用的键)和一个值。这里用emit将文档中某个键的计数(count)返回({count : 1})。我们想为每个键单独计数,所以为文档中的每一个键调用一次emit。this就是当前映射文档的引用:

    > map = function() {
        for (var key in this) {
            emit(key, {count : 1})
        }};

    这样就有了许许多多{count : 1}文档,每一个都与集合中的一个键相关。这种由一个或多个{count : 1}文档组成的数组,会传递给reduce函数。reduce函数有两个参数,一个是key,也就是emit返回的第一个值,还有另外一个数组,由一个或者多个对应于键的{count : 1}文档组成。

    > reduce = function(key, emit) {
        total = 0;
        for (var i in emits) {
            total += emits[i].count;
        }
        return {"count" : total};
        }

    reduce一定要能被反复调用,不论是映射(map)环节还是前一个简化(reduce)环节。所以reduce返回的文档必须能作为reduce的第二个参数的一个元素。
    reduce能处理emit文档和其他reduce结果的各种组合。
    MapReduce函数类似这样:

    > mr = db.runCommand({"mapreduce" : "foo", "map" : map, "reduce" : reduce})
    {
        "result" : "tmp.mapreduce_1266787811_1",
        "timeMillis" : 12,
        "counts" : {
            "input" : 6,
            "emit" : 14,
            "output" : 5
        },
        "ok" : true
    }

    MapReduce返回的文档包含很多与操作有关的元信息:
    ·"result" : "tmp.mapreduce_1266787811_1"
    这是存放MapReduce结果的集合名。这是一个临时集合,MapReduce的连接关闭后自动就被删除了。
    ·"timeMillis" : 12
    操作花费的时间,单位是毫秒。
    ·"count" : { ... }
    这个内嵌文档包含3个键。
    ·"input" : 6
    发送到map函数的文档个数。
    ·"emit" : 14
    在map函数中emit被调用的次数。
    ·"output" : 5
    结果集合中创建的文档数量。
    "count"对调试非常有帮助。
    对结果几核进行查询会发现原有集合的所有键及其计数:
    > db[mr.result].find()
    { "_id" : "_id", "value" : {"count" : 6} }
    { "_id" : "a", "value" : { "count" : 4 } }
    { "_id" : "b", "value" : { "count" : 2 } }
    { "_id" : "x", "value" : { "count" : 1 } }
    { "_id" : "y", "value" : { "count" : 1 } }
    每个键值变为一个"_id",最终花间步骤的结果变为"value"。
    【MapReduce例2:网页分类】
    假设有一个网站,人们可以提交其他网页的链接,比如rebbit.com,提交者可以给这个链接做标签,表明主题,比如"politics","geek"或者"icanhascheezburger",可以用MapReduce找出哪个主题最为热门,热门与否由最近的投票决定。
    首先,建立一个map函数,发出(emit)标签和一个基于流行度和新近成都的值。

    map = function() {
        for (var i in this.tags) {
            var recency = 1/(new Date() - this.Date);
            var score = recency * this.score;
            
            emit(this.tags[i], {"urls" : [this.url], "score" : score});
        }
    };

    现在就简化同一个标签的所有值,形成这个标签的分数:

    reduce = function(key, emits) {
        vat total = {urls : [], score : 0}
        for (var i in emits) {
            emits[i].urls.forEach(function(url)) {
                total.urls.push(url);
            }
            total.score += emits[i].score;
        }
        return total;
    }

    最终的集合包含每个标签的URL列表和表示该标签流行程度的分数。
    -- MapReduce部分没有完全掌握! --
    『MongoDB和MapReduce』
    前面两个例子只用到了mapreduce、map和reduce键。这三个键是必须的,除此之外MapReduce命令还有很多可选的键。
    ·"finalize":函数
    将reduce的结果发送给这个键,这是处理过程的最后一步。
    ·"keeptemp":布尔
    连接关闭时临时结果集合是否保存。
    ·"output":字符串
    集合结果的名字。设定该项则隐含着keeptemp : true。
    ·"query":文档
    会在发往map函数前,先用指定条件过滤文档。
    ·"sort":文档
    在发往map前先给文档排序(与limit一同使用非常有用)。
    ·"limit":整数
    发往map函数的文档数量的上限。
    ·"scope":文档
    JavaScript代码中要用到的变量。
    ·"verbose":布尔
    是否产生更加详尽的服务器日志。
    ⒈finalize函数
    finalize会在最后reduce得到输出后执行,然后将结果存到临时集合中。
    ⒉保留结果集合
    设置keeptemp为true或者设置out选项给集合取个好点的名字。
    ⒊对文档子集执行MapReduce
    有时候需要对集合的一部分执行MapReduce。只需要在传给map函数前添加一个查询来过滤一下文档就好了。
    过滤主要就是用"query"、"limit"和"sort"键指定。
    "query"键的值是一个查询文档。通常查询返回的结果就传递给了map函数。例如,有个应用程序做跟踪分析,需要上周的概要,只要使用如下命令对上周的文档执行MapReduce就好了:
    > db.runCommand({"mapreduce" : "analytics", "map" : map, "reduce" : reduce, "query" : {"date" : {"$gt" : week_ago}}})
    sort选项一般和limit一铜发挥重要作用。limit也可以单独使用,用来截取一部分文档发送给map函数。
    如果在上个例子中想分析最近10000个页面视图(而不是最近一周的),则可以借助limit和sort:
    > db.runCommand({"mapreduce" : "analytics", "map" : map, "reduce" : reduce, "limit" : 10000, "sort" : {"date" : -1}})
    query、limit、sort可以随意组合,但要是没有limit,sort单独使用的用处不大。
    ⒋使用作用域
    例:在之前的一个例子中,用1/(new Date() - this.date)计算了页面的新近程度。还可以将当前的日期作为作用域的一部分传递进去:
    > db.runCommand({"mapreduce" : "webpages", "map" : map, "reduce" : reduce, "scope" : {now : new Date()}})
    这样,在map函数中就能计算1/(now-this.date)了。
    ⒌获得更多的输出
    如果想看看MapReduce的运行过程,可以用"verbose" : true。
    也可以用print把map、reduce、finalize过程中的信息输出到服务器日志上。

  • 相关阅读:
    web页面中四种常见必测控件
    python03
    python基础2
    python基础
    如何定位测试用例的作用?
    需求测试的注意事项有哪些?
    性能测试的流程?
    简述bug的生命周期?
    Python字符串
    Python基础语法
  • 原文地址:https://www.cnblogs.com/answernotfound/p/mongodbnote5.html
Copyright © 2020-2023  润新知