Mongo In Action--查询与聚合

来源:互联网 发布:淘宝店铺引流量方法 编辑:程序博客网 时间:2024/06/15 20:06

         上一篇文章我们讲述了文档和文档插入相关的内容,本篇我们继续学习MongoDB中关键的查询和聚合功能。基于MongoDB实战-面向文档的数据(找到最合适的数据建模方式)这篇博文中的建立的电子商务模型。本章将讲述基于它进行很多不同的查询,包括_id查询,范围查询,排序和投影(Projection)。除了查询,我们还会涉及聚合(aggregation)主题。查询允许你获得存储的数据,聚合函数则能汇总并重新组织那些数据。还会介绍MongoDB的分组和MapReduce函数。

1. 电子商务查询

       MongoDB实战-面向文档的数据这篇博文中,我们给出了电子商务数据模型。我们已经为产品、分类、用户、订单和产品评论定义了文档结构。下面针对这些文档数据进行对应的查询操作。

1.1 产品、分类和评论

     大多数电子商务应用程序都提供至少两种基本的产品和分类视图。第一种是产品主页,突出某个指定的产品,显示其评论,给出一些与产品分类相关的信息。第二种是产品列表界面,允许用户浏览分类层级,查询所有分类中所有产品的缩略图。先进入对应的数据库product,use product过后,执行下述指令

product=db.products.findOne({'slug':'wheel-barrow-9092'})db.categories.findOne({'_id':product['main_cate_id']})db.review.findOne({'product_id':product['_id']})
执行结果如下:




若将最后的查询换为find方法,执行结果如下:


在使用查询方法时,注意区分find和findOne的区别。find返回的是游标对象;findOne返回的是一个文档,下面的两个查询语句是等价的。

product=db.products.findOne({'slug':'wheel-barrow-9092'})product=db.products.find({'slug':'wheel-barrow-9092'}).limit(1)
如果仅仅想要一个文档,只要它存在,findOne就能找到它。如果需要返回多个文档,就要使用find方法了,该方法返回一个游标,你需要在应用程序里对它进行迭代。大多数应用程序会对返回结果进行分页。为此MongoDB提供了skip和limit选项,可以像下面这样对评论文档进行分页

db.review.find({'product_id':product['_id']}).skip(0).limit(12)
为了保证review集合中具有足够的文档,使用Ruby批量插入的方式,一次插入了40个文档

require 'mongo'con=Mongo::Client.new(['127.0.0.1:27017'],:database=>'peoduct')logInfo=con['review']docs=(0..40).map do |n|{    :_id=>BSON::ObjectId.new,      :product_id=>BSON::ObjectId("59884b76b53fab2a8024b6ad"),      :date=>Date.new(2010,5,7),      :title=>"Amazing",      :text=>"Has a squeaky wheel,but still a darn good wheel barrow",      :rating=>4,      :user_id=>BSON::ObjectId("4a5b1476238d3b4dd5000001"),      :user_name=>"dgreenthumb",      :helpful_votes=>n,      :voter_ids=>[          BSON::ObjectId("59884b76b53fab2a8024b600"),          BSON::ObjectId("59884b76b53fab2a8024b601"),          BSON::ObjectId("59884b76b53fab2a8024b602")               ],    :time=>Time.now.utc }endlogInfo.insert_many(docs)
文档插入后再次执行上述分页查询语句,结果如下:


    还有一种应用场景,就是首先查询一个指定的类,然后获取指定类的同级分类,以及该类下的所有产品,完整的程序如下:

category=db.categories.findOne({'slug':'outdoors'})slibings=db.categories.find({'parent_id':category['parent_id']})products=db.products.find({'category_id':category['_id']}).skip((page_number-1)*12).limit(12).sort({'averegr_review':-1})

1.2 用户与订单

     在上面部分的学习中,查询主要限于_id查找和排序,对于用户和订单,由于希望为订单生成基本的报表,我们希望查询更进一步。在常规的身份验证中,我们提供用户名和密码到应用程序中,一般使用以下查询语句

db.user.findOne({'last_name':'kbanker','hashed_password':'bd1cfa194c3a603e7186780824b04419'})
      对于上面的查询,如果用户名和密码正确,会返回完整的文档;否则就没有返回结果。如果我们只想要返回_id字段,就可以使用投影来限制返回的字段:

db.user.findOne({'last_name':'Banker','hashed_password':'bd1cfa194c3a603e7186780824b04419'},{'_id':1})
这时响应文档里只有_id字段返回了。

      如何实现模糊匹配,即部分查询。SQL中的like操作,在MongoDB等价于对应的正则表达式。

db.user.find({'last_name':/^Ba/})
     如果要进行根据邮政编码进行范围查询,可以使用如下的操作语句,若要查询更高效,可以在address.zip上建立索引:

db.user.find({'address.zip':{$gte:10019,$lt:10040}})
     根据地域来寻找目标用户未必是提升转化率的最好途径,根据用户买过的东西进行分组会更有意义。这会需要两步查询:首先,基于特定产品或得一个订单集合一旦有了订单,就能查询关联的用户了。比如查询指定的SKU

db.orders.find({'line_items.sku':'9092'})
     若想获取时间在某个区间段的订单信息,只需简单地添加一个查询条件,指定最小的订单日期
db.orders.find({'line_items.sku':'9092','purchase_date':{$gte:new Date(2009,0,1)}})
     如果这些查询很频繁,需要一个复合索引,先按照SKU排序,然后再按照购买日期排序,可以按照下面的程序创建索引

db.orders.ensureIndex({'line_items.sku':1,'purchase_date':1})
     在查询orders集合的时候,所寻找的就是用户的ID列表。因此使用投影会更高效一些。下面的代码中,先规定只要user_id字段,然后将查询结果转换为一个简单的ID数组,随后使用$in操作符查询users集合

user_ids=db.order.find({'line_items.sku':'9092','purchase_date':{'$gt':new Date(2009,0,1)}},{'user_id':1,_id:0}).toArray().map(function(doc) {return doc['user_id']})
users=db.users.find({_id:{$in:user_ids}})
2. MongoDB查询语言

2.1 查询选择器

2.1.1 选择器匹配

       要指定一条查询,最简单的方法就是使用选择器,其中键值对直接匹配要找的文档。如下面两个例子:

db.user.find({'last_name':"Banker"})db.user.find({'first_name':"Smith",'age':40})
      无论传入多少个键值对,他们必须全部匹配;查询条件之间相当于运用了布尔运算符and。

2.1.2 范围查询

     我们经常需要查询某些值在一个特定范围内的文档。在SQL中,可以使用<,<=,和>=;在MongoDB中有类似的操作符:$lt,$lte,$gt和$gte。贯穿全书,我们都在使用这些操作符,它们的行为与预期的一样。但初学者在组合使用这些操作符时偶尔会很费力,常见的错误是重复搜索键:

db.user.find({'age':{'$gte':0},age:{'$lte':30}})
      因为同一文档中,同一级不能有两个相同的值,所以这个查询选择器是无效的,两个范围操作只会应用其中之一。正确的表达方式如下:

db.user.find({'age':{'$gte':0,'$lte':30}})
      还有一个值得注意的地方:范围操作符涉及了类型,仅当文档中的值与要比较的值类型相同时,范围查询才会匹配该值。只要同一集合中永远不会为同一个键保存多种类型,就可以不用担心这条类型限制。

2.1.3 集合操作符

       $in、$all、$nin这三个查询操作符接受一到多个值的列表,将其作为谓词。如果任意给定值匹配搜索键,$in就该返回该文档。$in经常会被用于ID列表。$nin仅在于给定元素都不匹配的时候才返回文档。最后,当搜索键与每个给定元素都匹配时,$all才会返回文档。下面为三个操作符的用法提供几个简单的示例

db.products.find({mian_cat_id:{$in"[ObjectId("XXXX"),ObjectId("XXXX"),ObjectId("XXXX")]}})db.products.find('detail.color':{$nin:['balck','blue']})db.products.find('tags':{$all:["gift","gardent"]}))
2.1.4 布尔操作符

        MongoDB的布尔操作符包括$ne,$not,$or,$and和$exists。$ne表示不等于,在实践中,最好和其他操作符结合使用,否则查询效率可能不高:因为它无法使用索引。$ne可以作用于单个值和数组;$ne匹配特定值以外的值,而$not则是对另外一个MongoDB操作符或正则表达式查询的结果求反。在使用 $not前,请记住大多数查询操作符已经有否定形式了($in和$nin,$gt和$lte等),$not不应该和它们搭配使用。当你所使用的操作符或正则表达式没有否定形式时,才应使用$not。$or表示两个不同键对应的值的逻辑或关系。其中重要的一点是:如果可能的值限制在同一个键里,使用$in代替。

db.products.find('detail.color':{'$in':['blue','green']}db.products.find({'$or':[{'detail.color':'blue'},{'details.manufacturer':'ACME'}]})
$or接受一个查询选择器数组,每个选择器的复杂度随意,而且可以包含其他查询操作符。和$or一样,$and操作符同样接受一个查询选择器数组。对于包含多个键的查询选择器,MongoDB会对条件进行与计算,因此只有在不能简单地表示AND关系时才应该使用$and.例如想要查询所有标记有gift或holiday,同时还有gardening或landscaping的产品。表示该查询的唯一途径是关联两个$in查询

db.products.find({$and:[{tags:{$in:['gift','holiday']}},{tags:{$in:['gardening','landscaping']}}]})
$exists用于查询集合中是否包含特定键的文档。
2.1.5 匹配子文档

      可以通过.操作符来进行子文档查询,此类查询可以指定任意的深度。假设文档中出现了复合键,那么查询时的键值顺序的不同会影响查询结果。若有下列文档和两条查询语句

{_id:{sym:'GOOG',date;20101015},open:40.23,high:45.50,low:38.81,close:41.22}db.ticks.find({_id:{sym:'GOOG',date:20101015}})db.ticks.find({_id:{date:20101015,sym:'GOOG'}})
两个查询语句并不等价。虽然Shell中输入的JSON文档的键顺序会被保留,但是不是所有语言驱动的文档表述都是如此。若要在Ruby1.8中保留键顺序,请使用BSON::OrderedHash

2.1.6 数组

       数组使得文档模型更加强大,如你所见,数组可以用来存储字符串列表、对象ID列表,甚至其他文档的列表。数组能带来更丰富、更易于理解的文档;按照常理,MongoDB能够轻松地查询并索引数组。事实也是如此,最简单的数组查询就和其他文档类型的查询一样。要将多个条件限制在同一个子文档上,可以使用$elemMatch操作符:

db.user.find({'address.name':'home','addres.state':'NY'})db.user.find({'address':{$elemMatch:{name:'home',state:'NY'}}})
从逻辑上来看,只有在匹配子文档中的多个属性时才会使用$elemMatch操作符。数组操作符$size,该操作符能让我们根据数组大小进行查询,例如:我们想要找出所有带有三个地址的用户,可以这样使用$size操作符:

db.user.find({'addresses':{$size:3}})
2.1.7 JavaScript

     如果目前为止的工具都无法表示你的查询,那你就可能需要写一些JavaScript了。我们可以使用特殊的$where操作符。向任意查询中传入一个JavaScript表达式。在JavaScript上下文里,关键字this指向当前文档,使用JavaScript无法使用索引。例如:

db.review.find({$where:"function() {return this.helpful_votes >3; }"})
该查询还有一个简化形式

db.review.find({$where:"this.helpful_votes>3"})
应该只在无法通过标准查询语言表示查询时才使用JavaScript查询。如果确实有需要,请尝试为JavaScript表达式带上至少一个标准查询操作符。标准查询操作符可以缩小结果集,减少必须加载到JS上下文的文档。例如:

db.review.find({user_id:ObjectId("XXXX"),$where:{"this.rating*.92"}>3})
上述查询满足了之前的两条建议,在user_id字段上使用了标准查询,这个字段一般有索引的;在超出标准查询语言能力的情况下使用了JavaScrpt表达式。但是在使用过程中,除了要识别出额外的性能开销,还要意识到JavaScript注入攻击的可能性。
2.1.8 正则表达式

     在前面的例子中,我们使用/^Ba/来匹配以Ba开头的字符串。使用的是前缀表达式。除了这种形式,正则表达式都用不上索引。因此,建议在使用是和JavaScript一样,结合至少一个其他查询项。下面的例子中,将查询指定用户的包含best或worst文字的评论。请注意,该处使用了正则表达式i来表示忽略大小写。

db.review.find({user_id:ObejctId("XXX"),text:/best|worst/i})
若环境中不支持原生的 正则表达式,可以使用特殊的$regex和$options操作符。

db.review.find({user_id:ObejctId("XXX"),text:{$regex:"best|worst",$options:"i"})
2.1.9 其他操作符

      还有两个查询操作符难以归类,第一个是$mod,允许查询匹配指定取模操作的文档。举例来说,可以使用下列查询来找出所有小计能被3整除的订单:

db.orders.find({'subtotal':{$mod:[3,0]}})
我们看到$mod操作符接受两个值组成的数组,第一个值是除数,第二个值是期望的余数。如果要使用$mod操作符,牢记它无法使用索引。
     第二个操作符时$type,根据BSON类型来匹配值,我不建议为一个集合的同一个字段保存多种类型,但是如果发生这样的情况,可以用这个操作符来检查类型。

2.2 查询选项

      所在的查询都要有一个查询选择器。就算没有提供,查询本身就是由查询选择器定义的。但是在发起查询时,有多种查询选项可供选择,他们进一步约束结果集。

2.2.1 投影

       在查询结果集中,我们可以使用投影来选择字段的子集进行返回。当有大文档时就更应该使用投影,这样能最小化网络延时和序列化开销。通常是用要返回的字段集合来定义投影。

db.users.find({},{username:1})
该查询结果将包含两个字段username和_id,默认情况下,_id字段总是包含在返回结果中的。在某些情况下,你可能希望排除特定字段。可以在投影中增加这些字段,并将结果设置为0

db.users.find({},{address:0,payment_methond:0})
        除了包含和排除字段,还能返回保存在数组中的某个范围内的值。例如,我们可能想要在产品文档中保存产品评论,同时还希望能对那些评论进行分页,为此可以使用$slice操作符,要返回头12篇和倒数5篇评论,可以这样使用$slice

db.products.find({},{reviews:{$slice:12}})db.products.find({},{reviews:{$slice:-5}})
$slice还能同时接受两个元素的数组,分别表示跳过的元素数和返回元素个数限制,下面演示如何跳过头24篇评论,并限制返回12篇评论

db.products.find({},{reviews:{$slice:[24,12]}})
注意$slice并不会阻止返回其他字段,如果希望文档中限制其他字段,必须显示的进行控制

db.product.find({},{reviews:{$slice:12},'reviews.rating':1})
2.2.2 排序

       所有的查询结果都能根据一个或多个字段进行升序或降序排列。除了对单个字段进行排序,对多个字段的组合排序,顺序至关重要。

2.2.3 skip和limit

    在向skip传递很大的值是需要注意,执行这种查询要扫描和skip值等量的文档。
2.3 聚合指令

        我们使用个使用group和map-reduce编写脚本实现各种聚合函数,从简单的求和到计算标准差。

2.3.1 根据用户评价进行分组

      group最少需要三个参数,第一个参数是key,定义如何对数据进行分组。第二个参数是一个对结果集做聚合的JavaScript函数,叫reduce函数。第三个分组参数是reduce函数的初始文档。首先我们的初始文档和reduce函数如下

initial ={review:0,votes:0}reduce=function(doc,aggregator){    aggregator.reviews+=1.0;    aggregator.votes+=doc.votes;}
下面的示例展示了group命令

results=db.review.group({    key:{user_id:true},    initial:{review:0,votes:0.0},    reduce: function(doc,aggregator)      {      aggregator.review+=1;     aggregator.votes+=doc.helpful_votes;},finalize: function(doc){      doc.average=doc.votes/doc.review;}})
此处,我们额外向group传递了一个参数,我们希望获得每评论的平均得票数,但在计算出总的评论得票数和评论总数之前,我们无法得到改值。这就是终结器(finalizer)的原因,它是一个JavaScript函数,在group命令返回前应用于每个分组结果上。在MongoDB上执行结果如下:


2.3.2 根据地域对订单应用MapReduce

        我们可以把MonggoDB的Map-Reduce当做更灵活的group。有了map-reduce,可以更细粒度地控制分组键,还有大量输出选项可用,包括将结果保存在新的集合里,以便后续能够获取那些数据。

         我们有时希望生成一些销售汇总,可以以此为例。正如map-reduce函数名字所暗示的,第一步就是编写一个映射函数,应用于集合中的每个文档,在此过程中实现两个目的:定义分组所用的键;整理计算所需的所有数据,定义为变量

var map =function(){var shipping_month=this.purchase_date.getMonth()+"-"+this.purchase_date.getFullYear();var items=0;this.line_iltems.forEach(function(item){     items+=item.quantity;});emit(shipping_month,{order_total:this.sub_total,items_total:items});}
首先,需要知道变量this总是指向正在迭代的文档。在函数第一行,我们获取了一个表示订单创建月份的整数。随后调用了emit(),这是每个映射函数必须要调用的特殊方法。emit()的第一个参数是分组依据的键,第二个参数通常是包含要执行reduce的值的文档。本例子中,我们要根据月份分组,对每个订单的小计和明细数量做统计。下面是对应的reduce函数,定义为变量

var= reduce=function(key,values){var tmpTatal=0;var tmpItems=0;values.forEach(function (doc){tmpTotal+=doc.order_total;tmpItems+=doc.items_total;});return ({total:tmpTotal,items_total:tmpItems});}
使用方式:

filiter=[purcahse_date:{$gt:new Date(2010,0,1)}]db.orders.mapReduce(map,reduce,{query:filiter,out:'totals'})
依次执行后,会生成一个存放结果的集合totals,执行结果和查询结果如下:



2.4 详细讲解聚合函数

2.4.1 max(),min()

MongoDB中没有实现max和min,必须自己实现。如要找到某个字段的最大值,可以按照该字段降序排序,并限制返回一个文档;相反,按升序排序并限制返回一个文档,实现找到最小值

2.4.2 distinct

   MongoDB的distinct指令是获取特定字段中不同值列表的最简单工具。该命令既适用于单键,也适用于数组键。distinct默认覆盖整个集合,但也可以通过查询选择器进行约束。可以像下面一样使用distinct获取产品集合里所有唯一标签的列表:

db.products.distinct("tags")
如果希望操作products集合的一个子集,可以传入一个查询选择器作为第二个参数。这里查询将不同的标签限定到Gardening Tools分类的产品中

db.products.distinct("tags",{category_id:ObjectId("XXXX")})
    在实用性方面,distinct和group有一个很大的限制:它们返回的结果集不能超过16MB,16MB的限制并不是这些命令本身所强加的阈值,这是所有的初始查询结果集大小。distinct和group是以命令的方式实现的,也就是对特殊的$cmd集合的查询,它们赖以生存的查询则受限于该限制。如果distinct或group处理不了你的集合结果集,那么只能使用map-reduce代替了,它的结果可以保存在集合中而非内联(inline)返回。
 2.4.3 group

       group和distinct一样,也是数据库命令,因此它的结果受限于16MB响应限制。而且,为了减少内存消耗,group不会处理多余10000个唯一键。如果聚合操作在此范围内,group是个不错的选择,因此通常情况下它比map-reduce快。

     group的选项

     key:描述分组字段的文档,例如,要根据category_id分组,可以将{category_id:true}作为键。此处还可以是复合键,比如,若想根据user_id和rating对一系列帖子作分组,键看起来是这样的:{user_id:true,rating:true}。除非使用了keyf,否则key是必须的。

     keyf:这是一个JavaScript函数,应用于文档之上,为该文档生成一个键,用于分组的键需要计算时,这个函数非常有用。举例来说,如果你想根据每个文档创建时是周几来对结果集进行分析,但又不实际存储改值,就可以使用键函数来生成这个键:

function(doc){return {day:doc.created_at.getDay();}}
这个函数会生成类似{day:1}这样的键。如果没有指定标准的key,那么keyf是必需的。

     initial:作为聚合结果初始值的文档。reduce函数第一次运行时,该初始文档会作为聚合器的第一个值,通常会包含所有要聚合的键。距离来说,如果正在为每个分组计算总投票数和总文档数,那么初始文档看起来是这样的:{vote_sum:0.0,doc_county:0)}。该参数是必需的。
     reduce 用于执行聚合的JavaScript函数。该函数接受两个参数:正被迭代的当前文档和用于存储集合结果的聚合器。聚合器的初始值就是初始文档。请注意reduce函数并不返回任何内容,它不过是修改聚合器对象,reduce函数也是必需的。

    cond:过滤要聚合文档的查询选择器,如果不希望分组操作处理整个集合,就必须提供一个查询选择器。

    finalize,在返回结果之前应用于每个结果文档的JavaScript函数。该函数支持对分组操作的结果进行后置处理。通常会用它计算平均值,在分组结果的现有值之外,再增加一个值来保存平均值。

function(doc){      doc.average=doc.votes/doc.review;}
2.4.4 map-reduce

       对大数据集进行迭代,尤其是在分片配置中,需要分布式的聚合器,而MapReduce恰恰提供所需的内容。map-reduce包含很多选项,此处详细对这些选项做了说明:

   map:应用于每个文档之上的JavaScript函数。该函数必须调用emit()来选择聚合的键和值。在函数上下文中,this的值指向当前文档。例如想要根据用户ID对结果进行分组,计算出总投票数和总文档数,映射函数应该是这样的。

function(){emit(this.user_id,{vote_sum:this.vote_count,doc_cum:1});}
    reduce:一个JavaScript函数,接收一个键和一个值列表。该函数对返回值的机构有严格要求,必须总是与values数组所提供的结构一致。reduce函数通常会迭代一个值的列表,在此过程中对其进行聚合。请注意,通常聚合函数中不会用到key参数的值

     query:用于过滤映射处理的集合的查询选择器。该参数的作用与group的cond参数相同

     sort:对于查询的排序,与limit选项搭配时非常有用,这样就可以对1000个最近创建的文档进行map-reduce。

     limit: 一个整数,指定了查询和排序的条数

      out: 该参数决定了如何返回输出内容,要将所有输出作为命令本身的结果,传入{inline:1}。注意这仅仅适用于结果集符合16MB返回限制的情况。另一个选择是将结果放到一个输出集合里。此时,out的值必须是一个字符串,标明用于保存结果的集合的名称。

     将结果保存到输出集合时有一个问题:如果最近运行过类似的map-reduce,那么可能会覆盖现有的数据。因此,还有两个集合输出选项:一个用于和老数据合并结果,另一个对数据进行reduce处理。在合并场景中,使用{merge:"collectionName"},新结果会覆盖拥有相同键的现有项。如果使用{reduce:"collectionName"},会调用reduce函数根据新值来处理现有的值。尤其在执行要反复运行的mapreduce任务时,希望把新集合整合到现有聚合之中,reduce格外有用。在对集合执行新的MapReduce任务时,只需简单添加一个查询选择器来限制聚合所需的数据集。

     finalize: 一个JavaScript函数,在reduce阶段完成后会应用于每个返回的文档上。

     scope: 该文档指定了map,reduce和finalize函数可全局访问的变量的值。

     verbose:一个布尔值,为true时,在命令返回文档中会包含对map-reduce任务的执行时间的统计信息。















        










         

原创粉丝点击