`
QING____
  • 浏览: 2228368 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Mongodb中Index特性

 
阅读更多

    Mongodb支持多种index类型,这相对于其他Nosql数据库而言具有很大的优势,它的索引类型比较接近SQL数据库,所以开发者在mongodb中使用索引将是非常便捷的。索引最大的作用就是提高query的查询性能,如果没有索引,mongodb需要scan整个collection的所有的documents,并筛选符合条件的document,如果有索引,那么query只需要遍历index中有限个索引条目即可,况且index中的条目是排序的,这对“order by”操作也非常有利。

    index是一种特殊的数据结构,它以一种易于“traverse”的形式来保存collection数据集的一小部分数据,index保存了指定field或者多个filed的值,并按照filed值的顺序排序,索引条目(entry)的有序性在“相等比较”、range查询操作中可以支撑较高的性能,mongodb可以根据index的顺序返回result。

    事实上,mongodb的索引结构和原理,和普通的数据库索引没什么太大区别,数据结构仍然采用B-TREE(以及变种)。

    尽管index可以提升query的性能,不过在创建索引时仍需要兼顾一些其他方面的考虑,如果你的collection已经保存了大量数据,此时创建索引将会导致大量的IO操作(内存,磁盘读写),耗时较长;mongodb提供了2种方式:foreground和background,foreground即前台操作,它会阻塞用户对数据的读写操作直到index构建完毕,即任何需要获取read、write锁的操作都会阻塞,默认情况下为foreground;background即后台模式,不阻塞数据读写操作,独立的后台线程异步构建索引,此时仍然允许对数据的读写操作;其中background比foreground更加耗时。我们可以使用此方法指定:

db.<collection>.createIndex({useid : 1},{background : true})

    

    同时在构建索引之前,最好检测一下index是否有重复的,或者当前query是否可以被现有的index覆盖,因为重建重复的index不能对提升性能有任何帮助。为了避免性能问题,我们需要在应用启动前通过getIndexes()方法检测相应的索引是否存在,通常我们需要使用单独的代码来创建索引,而不是在application中。如果在构建索引完成之前,mongodb异常退出,那么当mongodb重启时是否继续重建可以通过storage.indexBuildRetry(配置文件参数)或者--nonIndexBuildRetry(命令行参数)来决定,如果mongodb在构建是遇到错误,比如“dumplicate key error”这种无法自主兼容解决的,则会终止并退出进程。

collection.createIndex(new Document("useid",1),new IndexOptions().unique(true));##java代码
collection.createIndex(new Document("userid",1),new IndexOptions().name("idx_useid"));//指定index的别名

    我们可以使用dropIndex来删除索引信息:

collection.dropIndex("idx_userid");##使用index name删除
collection.dropIndex(new Document("userid",1));

    同时mongodb还提供reIndex命令,可以重建index,它的原理就是先drop索引,然后重新创建索引。

    遍历索引:

ListIndexesIterable<Document> indexes = collection.listIndexes();
MongoCursor<Document> cursor = indexes.iterator();
while (cursor.hasNext()) {
      Document index = cursor.next();
}
cursor.close();

    mongodb中提供了hint()方法,可以强制query使用指定的索引,这在某些场景下有特殊的作用。此外,mongodb像其他database一样提供了explain()方法,用来检测query对索引的选择情况。此处不再赘言。 

 

 一、索引类型

    1、_id:每个document都必须有_id字段,如果不指定,mongodb会默认生成;_id字段为内置的“唯一索引”,整个collection中不会存在_id值相同的document,这是mongodb的内部机制,开发者无法改变,也不需要开发者显式的为_id建立索引,当然也无法remove它。通过_id字段来查询document是性能极高的,所以如果尽可能的通过_id查询或者update数据。

 

    2、单字段索引(Single Filed):即对单个filed建立索引,也是常说的“普通索引”;建立索引时可以指定索引数据的order:正序还是倒序。比如:db.<collection>.createIndex({"score" : 1})表示对score字段创建索引,value值正序存储(1表示正序,-1表示倒序)。

 

    3、组合索引(Compound):对多个filed建立索引,即一个索引entry中包含多个filed值;比如{userid : 1, score : -1}表示对userid和score两个field建立索引,索引条目按照userid正序排列,相同的userid的索引条目再根据score值倒序排列。需要注意组合索引中filed的顺序很重要,需要根据application查询的需要慎重设计,否则可能导致无法使用索引而导致性能低下,这涉及到sort操作问题。

    mongodb的一个组合索引最多支持31个字段。

    组合索引中字段的组合顺序很重要,这和数据库索引一样,索引的匹配仍然遵循“最左前缀”原则。

    索引的存储按照字段值的顺序,比如{useid : 1,score : -1},所有索引条目将首先按照userid正序排列,相同的useid按照score字段值倒序排列;对于单子段索引,字段值的排序方式无论是正序还是倒序,并没有太大影响,因为mongodb可以很简单的来“反转”方向;但是对于组合索引,这种排序方式将直接决定是否支持query的sort操作。上述索引可以支持如下查询:

db.user.find().sort({userid:1})
db.user.find().sort({userid:1,score:-1})
db.user.find().sort({userid:-1,score:1})##排序方向反转
#但不支持
db.user.find().sort({userid:1,score:1})
db.user.find().sort({userid:-1,score:-1})

    最左前缀(prefix):索引前缀即必须以一个或者多个字段为开头,比如组合索引{"item" : 1,"location" : 1,"stock": 1},它可以支持query中过滤条件包含:1)item字段 2)item字段和location字段 3)item、location、stock三个字段;如果query过滤条件中不包含item字段将无法使用索引,比如location、stock或者location + stock字段的查询条件。

 

    4、复合键索引(Multikey):对document中的数组字段创建索引,就是“复合键索引”;mongodb将会对数组的每个值创建一个索引条目,但每个条目都引用同一个document。复合键索引允许查询数组中是否包含某个(些)元素;如果创建索引的filed为数组,mongodb会自动创建multikey索引,我们不需要显式指定任何额外的属性。如果已经对某个字段建立了复合键索引,那么此后尝试插入新文档但是此字段不是数组时将会抛出error。不需要需要注意,如果索引包含多个字段时(组合索引),只能有一个字段的值为数组,同时对多个数组字段建立复合键索引将不予支持。

    在sharding cluster架构中,复合键所以将不能作为sharding key(分片键),或者说数组字段不能作为分片键;复合键(数组字段参与索引)也不能是hash索引,也不支持“Covered query”,很好理解,因为这在存储层面几乎是无法做到的!!

    比如文档:{_id : 1,item: "ABC", ratings: [2, 5, 9]};创建索引{ratings: 1},那么对于此单个document而言,将有3个索引key(索引条目):2、5、9,每个索引key都指向同一个文档。此时我们也许已经了解了mongodb是如何构建复合键索引的数据结构了。

 

    5、地理空间索引(Geospatial):为了支持高效的地址位置数据查询,mongodb提供了2种特殊的索引,2d索引(坐标平面)和2sphere索引(经纬球面),存储数据包括经纬度信息(-180 ~ 180)以及数据表示的几何类型;在目前LBS应用中,通过使用Geo索引,可以实现类似于“附近的人、消息”等功能,我们可以利用现有的地图服务获取位置的经纬度信息,同时配合mongodb Geo索引来完成我们的部分业务应用。不过,mongodb并不是专业的LBS数据库。“2d”和2dsphere两种索引要求的document数据结构是不同的,所以开发者需要注意,我们稍后介绍。

    2dsphere:经纬球面,如果数据是球面的位置(球面几何),可以使用此索引,2dsphere索引可以与其他多个字段组合。location的数据结构可以为GeoJSON对象或者为传统的坐标对。【参见GeoJSON】mongodb支持如下几种GeoJSON对象:

    1)Point:点

    2)LineString:线段

    3)Polygon:多边形

    4)MultiLineString:多条线段

    5)MultiPoint:多点

    6)MultiPolygon:多个多变形

    7)GeometryCollection

 

    2d:平面坐标,在平面几何(欧几里得几何)中计算距离,我们可以存储一些传统的坐标对,这时就可以使用2d索引,2d索引最多只能与一个其他的字段组合,且2d字段必须做前缀,这与2dsphere是有区别的。不过2d索引是mongodb的旧版特性,在2.6之后,我们可以统一使用2dsphere即可,2d索引可以通过转换成GeoJSON中Point类型。

    我们使用Geo索引对坐标数据进行如下类型的查询:

    1)包含:可以查询包含(inclusion)在指定的多边形区域内的locations(所对应的documents,或者indexes),使用$geoWithin操作符。2d和2dsphere都支持包含查询。

    2)相交(intersection):可以查询与指定的几何体相交的locations,仅支持球面数据,即2dsphere索引,使用$geoIntersects操作。

    3)临近(proximity):可以查询与指定的point最近的其他points,使用$near操作,2d和2dsphere都支持。

    

    Geo两种索引都不能作为shard key(sharding cluster模式中),当然可以存储在sharded collection中,只是不能作为shard key。不过在shared collection中,$near和$nearSphere将不支持,而是使用$geoNear替代。(稍后专门介绍这些指令)

 

    6、文本索引(Text):支持对collection中的string内容进行搜索操作,即text search;mongodb内置支持几种语言的分词,不过这种文本搜索的能力与专业的搜索引擎相比,还相当简陋;此外mongodb的文本索引,不支持中文,关于此索引,我们将不再赘言。

 

    7、哈希索引(Hash):主要使用场景为“hash sharding”,即在“Sharding”架构模式中,对sharding key使用hash索引,以便数据的sharding和路由操作。Hash索引只支持值的“相等比较”,不支持range查询。当然如果你确定application的所有query均为某个field的“相等”匹配,也可以为此filed建立hash索引。hash索引只支持一个字段,它不能和其他字段组合,同时也不能是“unique”,使用db.<collection>.createIndex({a : "hashed"})来创建hash索引。

 

二、索引属性(property)

    1、唯一索引(Unique):即索引字段的值是唯一的,全局无重复;如果尝试存入重复的值(write),操作将会被拒绝。可以通过db.<collection>.createIndex({ "userid" : 1}, {unique : true})创建唯一索引。我们可以对一个组合索引使用unique属性,表示它们的组合值不能重复,默认情况下unique为false。如果插入的document不包含某个索引字段,那么此字段的值保存为null,mongodb也会用unique约束这样的文档,即不包含某个字段的文档只能保存一条。比如唯一索引{"x" : 1, "y" : 1},只能插入一条{"x" : "content"}(等同于{"x" : "content","y" : null})。对于hash索引而言,则不能使用unique属性。

    unique通常与sparse一起使用。

  

    2、稀疏索引(Sparse):只有当document中包含索引字段时,才会为此文档建立索引,但是这些文档仍然能够保存成功·;因为mongodb的document结构是模式自由的,所以document中不包含某个Field也是“正常的”;稀疏索引将会忽略那些document中不包含索引字段的记录,因此稀疏索引并没有包含collection中的所有文档;非sparse索引,将会为那些缺失的字段存储为null,因此索引包含了全部文档。

    我们可以将“Sparse”和“Unique”选项同时约束一个index,以避免包含重复值的document,同时还忽略那些不包含索引字段的document(不建立索引,但是文档仍然会被保存)。

    对于2d和2dsphere索引,通常是sparse。

    对于sparse属性的组合索引,只有至少包含一个索引字段的document才会建立索引,如果所有的索引字段都缺失,将不会为此文档建立索引。

    当sparse与unique同时约束索引时,比如createIndex({"score" : 1}, {sparse : true, unique: true}),如果文档中不包含score字段,那么文档都可以保存成功,但是不会对这些文档建立索引,但是对于包含了score字段的文档,那么score值则不能重复。如果是组合索引,比如createIndex({useid : 1, score : 1},{sparse : true, unique: true}),对于缺失score的文档,只要useid不同均可以建立索引且保存成功,如果useid和score都缺失,文档可以保存但是不会建立索引。

    我们已经了解到,sparse索引中可能没有包含所有的文档,因为部分文档可能因缺失索引字段而未能建立索引,但是它们缺失保存在了collection中,那么对于某些需要从全部documents筛选数据的操作将无法使用索引,比如查询语句{x : {$exists : false}}(查询不包含x字段的所有文档),除非你使用hint()方法强制使用此索引。

 

    3、TTL索引:对“date”、“timestamp”类型的字段建立TTL索引,并在索引选项中指定索引保存的时长,那么mongodb将会在一定时间之后自动移除那些“过期”的索引和documents。通过TTL索引,可以实现一个清洗数据的“定时任务”。TTL索引只能支持一个字段(同hash索引,不能对组合索引使用此特性),不能和其他字段组合,即一个索引中只能有一个字段;比如document中created字段类型为date,表示文档插入的时间,我们希望文档在3600秒后过期删除,那么可以通过db.<collection>.createIndex({ "created" : 1},{expireAfterSeconds : 3600})创建索引,其中“expireAfterSeconds”为TTL索引的必须选项。如果document中不包含索引字段,文档将不会过期。

    mongodb将会创建一个后台线程,读取index值并删除那些过期的document;在createIndex时,mongodb也会在构建索引的同时检测过期,删除那些过期的数据。TTL索引和众多的缓存服务一样,不能保证数据过期则立即被删除(对用户操作不可见),mongodb扫描数据(从头到尾)是有时间间隔的(60秒),因此过期数据的删除可能存在延迟问题。对于“replica Set”,这个后台线程只在primary上运行,primary上的删除操作同步给secondaries。mongodb的存储引擎的特性决定,这种大量的随机删除会导致严重的存储碎片,索引如果collection中存在TTL索引时,将会开启“usePowerOf2Sizes”选项而且不能修改,“usePowerOf2Sizes”可以降低存储碎片的可能。

collection.createIndex(new Document("created",1),new IndexOptions().expireAfter(15L,TimeUnit.MILLISECONDS));
##java代码

 

    对于java构建索引,代码样例如上,索引的属性可以通过IndexOptions来指定。

 

三、索引的设计目的就是提高query性能,稍后我们会介绍如何建立合适的索引,以及索引性能的分析。如果你了解过其他数据库的索引机制,可能还会提到“覆盖索引”(Covered query);如果查询条件和“projection”选项中只包含index字段,那么mongodb的查询结果只需要从索引条目中得到即可,无需scan任何document(磁盘操作、以及载入内存),这就是“Covered query”,性能很高。比如对字段score建立了索引,那么查询:collection.find({score:{"$lt" : 30}}, {score : 1,_id : -1})。

 

    索引交集(intersection):这种特性很多database也支持,即一个query使用多个indexes(通常为2个),取其交集作为result。比如一个query有多个查询字段作为过滤条件,一个index能够完成查询的一部分(过滤结果集),另一个index则完成另一部分,然后mongodb将这两个index使用交集来完成整个query;不过索引交集,是否能够提高查询性能,依赖于特殊的查询和系统本身,所以索引交集并非总是能提高性能,相反也会导致性能低下。

 

    对于replica set环境中,当primary上构建索引完成之后,将会在secondaries上构建,不过在secondaries上将采用background模式,因为background构建较大数据集合的索引非常耗时,我们可以首先将secondary离群,然后手动使用foreground方式构建,完成之后再次加入replica set集合,不过这里有个问题需要注意,primary将会把用户的所有write操作写入oplog中,这是一个capped collection,需要确保oplog collection的容量足够大,以避免secondary离群时间过长而导致无法增量的catch up,此时将会导致一个全量的initial sync(将会更加耗时)。

    对于sharded cluster而言,createIndex操作将会被发送到所有的shard-primary上。

 

四、Intersection

    上文已经简单提到了这个概念,对于很多数据库而言,也包含此特性,比如mysql。假如某个collection中有两个索引{qty : 1}和{item : 1},对于collection.find({item : "1234", qty : {$gt : 15}})这个查询语句将有可能使用上述两个索引的intersection来完成查询,不过具体是否真的会使用intersection(以及具体使用了那个索引)则需要explain()方法来检测,因为mongodb并不会总是使用intersection,因为它对性能的提高是不可预测的,有可能是降低性能的。

    在intersection中是否选择某个index,仍然遵循“最左前缀”原则。

 

五、Geo索引

    Geo索引可能会在基于LBS方面的应用上有所帮助,接下来我们专门介绍一下。

    1、2dsphere

    2dsphere数据格式可以为GeoJSON或者普通的坐标对,同时2dsphere是2d的兼容升级版,在新版本中我们直接使用2dsphere即可。2dsphere索引默认为sparse属性,索引可以与其他类型的字段形成组合索引,即一个组合索引中只能有一个字段为2dsphere。2dsphere(包括2d)字段不能作为sharding key,但是可以保存在sharding collection中。比如文档:

{
  loc : { type: "Point", coordinates: [ -73.97, 40.77 ] },
  name: "Central Park",
  category : "Parks"
}

    loc字段存储了一个GeoJSON格式的,有type和coordinates两个字段,分别表示“几何”的类型和坐标位置。通过createIndex({loc : "2dsphere"})创建索引,前文已经提到2dsphere可以与其他类型的字段一起构建成组合索引,2dsphere字段可以位于组合索引的任何位置(不像2d的组合索引,其中2d字段必须为索引的首个字段),比如createIndex({loc : "2dphere", category : -1});需要注意的是2dsphere索引字段存储的必须是GeoJSON对象或者普通的坐标对,对于普通的坐标对,数据结构为:

{
    loc : [-20.33, 30,12],
    category : "AAA",
    name : "BBB"
}

    2、2d

    2d索引数据能够存储普通的坐标对,这是mongodb较旧版本中支持的特性,它的使用场景类似与2dsphere中的Point数据类型;同时2d索引不支持GeoJSON数据对象,也不能使用2d索引查询collection中GeoJSON数据;此外2d索引和2dsphere一样不能作为sharding key,不过仍然可以在sharding collection中保存2d数据,这与2dsphere一样。不过2d字段参与组合索引时,只能作为第一个字段,这与2dsphere不同;比如document保存2d数据:

{
    loc : [-20.33, 30,12],
    category : "AAA",
    name : "BBB"
}

    可以通过createIndex({loc : "2d"}),或者组合索引createIndex({loc : "2d", category : -1})。通过数据结构可以知道,2d数据只能保存Point(点)类型的数据,其中坐标对分别为经度、维度,经度为第一个值,经纬度的值范围为[ -180,180)。

 

    3、GeoJSON对象

    在2dsphere中,我们知道GeoJSON是它所支持的标准数据类型,用来存储多种基于球面的地理位置数据(spherical),GeoJSON数据格式为:

{ type : <GeoJSON type>, coordinates : <coordinates>}

    type和coordinates两个字段都必填,type表示location的几何类型,coordinates表示其经纬度(经度在先,维度在后),根据type的不同,coordinates的数据结构也有所不同。接下来我们依次介绍几种常用的GeoJSON对象。对于java Driver,需要在3.1.x以上版本才支持GeoJSON API。

    1)Point(点

    表示一个坐标点,数据结构样例如下:

{ type : "Point", coordinates : [40,5]}

    其中coordinates为一个数组,表示坐标对。我们可以使用java插入一条Point类型的数据:

collection.insertOne(new Document("location",new Point(new Position(20.0,30.1))));

    2)LineString(线段)

    由2个Point组成,其中coordinates为Point数组

{ 
    "type" : "LineString", 
    "coordinates" : [[20.0, 30.0], [35.0, 45.0]] 
 } 

    插入一个LineString文档:

List<Position> points = new ArrayList<Position>();
points.add(new Position(20,30));
points.add(new Position(35,45));
collection.insertOne(new Document("location",new LineString(points)));

    3)Polygon(多边形)

    有多个LineString构成的闭合多边形,数据结构示例:

{
  type: "Polygon",
  coordinates: [ [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0  ] ] ]
}

   最终将有上述几个点依次连接,构成一个闭合的多边形,不过需要注意coordinates是一个数组,且数组的每个元素也是一个数组。

List<Position> positions = new ArrayList<Position>();
positions.add(new Position(0,0));
positions.add(new Position(3,6));
positions.add(new Position(6,1));
positions.add(new Position(0,0));//至少三个边,且最终闭合
collection.insertOne(new Document("location",new Polygon(positions)));

    在实际工作中,可能还存在多边形嵌套的情况,即图形有一个“外环”多边形和内环多边形组成,它们中间的区域为实际面积。这种嵌套关系,要求两个多边形的边不能有任何交集,且内环被外环完全包含

,数据结构如下:

{
  type : "Polygon",
  coordinates : [
     [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ],
     [ [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ] ]
  ]
}

    我们使用java插入一个这种环形多边形:

List<Position> exterior = new ArrayList<Position>();
exterior.add(new Position(0,0));
exterior.add(new Position(3,6));
exterior.add(new Position(6,1));
exterior.add(new Position(0,0));//外环

//[ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ]
List<Position> holes = new ArrayList<Position>();
exterior.add(new Position(2,2));
exterior.add(new Position(3,3));
exterior.add(new Position(4,2));
exterior.add(new Position(2,2));//中间的空洞
sphere.insertOne(new Document("location",new Polygon(new PolygonCoordinates(exterior,holes))));

    4)GeometryCollection(平面集合)

    这是一个特殊的数据结构,它表示有多种类型的“图形”组成的集合,比如“Point”、“Polygon”等等。数据结构示例如下:

{
  type: "GeometryCollection",
  geometries: [
     {
       type: "MultiPoint",
       coordinates: [
          [ -73.9580, 40.8003 ],
          [ -73.9498, 40.7968 ],
          [ -73.9737, 40.7648 ],
          [ -73.9814, 40.7681 ]
       ]
     },
     {
       type: "MultiLineString",
       coordinates: [
          [ [ -73.96943, 40.78519 ], [ -73.96082, 40.78095 ] ],
          [ [ -73.96415, 40.79229 ], [ -73.95544, 40.78854 ] ],
          [ [ -73.97162, 40.78205 ], [ -73.96374, 40.77715 ] ],
          [ [ -73.97880, 40.77247 ], [ -73.97036, 40.76811 ] ]
       ]
     }
  ]
}

    此例表示它由“MultiPoint”和“MultiLineString”两个图形构成。

    5)其他

    GeoJSON还有其他几种类型的对象,在此不再介绍,比如:MultiPoint表示多个点,MultiLineString表示多个线段,MultiPolygon表示多个多边形等等。

    4、Geo索引与查询指令

    Geo查询包含“临近”、“交集”、“包含”等等,我们通过使用如下几个专门的指令来完成相关查询。

    1)$geoWithin

    查询那些完全包含在指定形状(shape)内的地理空间数据的documents,允许有边界(border)重叠,$geoWithin通常指定Polygon或者MultiPolygon参与查询。我们指定的形状可以是一个Polygon类型的GeoJSON对象(单环,或者多环的),或者是一个MultiPolygon类型的GeoJSON。

    $geoWithin需要对查询字段建立geo索引,不过建立索引确实可以提高查询性能,2dsphere和2d索引均支持此操作。$geoWithin不会对结果数据进行排序(sort)。比如我们需要查询指定多边形内的所有地理空间数据(只查询GeoJSON类型的documents):

db.places.find(
   {
     loc: {
       $geoWithin: {
          $geometry: {
             type : "Polygon" ,
             coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]
          }
       }
     }
   }
)

    JAVA代码示例如下:

List<Position> positions = new ArrayList<Position>();
positions.add(new Position(0,0));
positions.add(new Position(3,6));
positions.add(new Position(6,1));
positions.add(new Position(0,0));//外环

collection.find(Filters.geoWithin("location", new Polygon(positions)));

    2)$geoIntersets

    查询与指定的GeoJSON对象表示的图形有“交集”(相交,区域重叠)的地理空间数据,包括仅仅有边界相交的图形,可以指定任意类型的GeoJSON类型参与查询。仅对2dsphere索引数据有效,仅查询类型为GeoJSON类型的documents。

db.places.find(
   {
     loc: {
       $geoIntersects: {
          $geometry: {
             type: "Polygon" ,
             coordinates: [
               [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ]
             ]
          }
       }
     }
   }
)

    JAVA代码示例为:

List<Position> positions = new ArrayList<Position>();
positions.add(new Position(0,0));
positions.add(new Position(3,6));
positions.add(new Position(6,1));
positions.add(new Position(0,0));//外环

collection.find(Filters.geoIntersects("location", new Polygon(positions)));

 

    3)$geometry

    配合$geoWithin、$geoIntersects、$near、$nearSphere使用,占位符,通过$geometry指定GeoJSON类型数据参与查询,即如果参与查询的不是GeoJSON,则不需要使用$geometry。我们在上述1)、2)中已经看到了对$geometry的使用例子。

    4)$near、$nearSphere

    这两个指令几乎一样,在查询条件中指定一个Point用来根据距离由近到远,获取相应的documents。这两个指令均可以查询GeoJSON对象,其中$near更倾向于对普通的坐标对查询(基于2d索引),$nearSphere倾向于对GeoJSON对象查询(2dsphere索引),不过$nearSphere也可以支持普通的坐标对。它们的查询样例为:

{
  $near: [ <x>, <y> ],
  $maxDistance: <distance in radians>
}

 

{
  $nearSphere: {
     $geometry: {
        type : "Point",
        coordinates : [ <longitude>, <latitude> ]
     },
     $minDistance: <distance in meters>,
     $maxDistance: <distance in meters>
  }
}
{
  $nearSphere: [ <x>, <y> ],
  $minDistance: <distance in radians>,
  $maxDistance: <distance in radians>
}

 

    JAVA代码样例为:

collection.find(Filters.near("location",new Point(new Position(20.1,30,0)),50d,20d));

    在查询条件中我们可以指定$minDistance和$maxDistance,最终处于2者距离之间的document将会由近到远的顺序作为result返回,其中$minDistance表示离指定的point至少多远,此指令仅仅当查询字段为2dsphere索引时才支持,即在2d索引下不能设置此值;$maxDistance表示最远的距离;对于$near,distance值为radians(弧度);对于$nearSphere,distance距离的单位是米,以指定的Point为圆心,distance为半径的圆所覆盖的地理位置数据(GeoJSON documents);如果$nearSphere应用在传统的坐标对上(2d数据),distance则为radians。 mongodb内部会有distance(米)与radians的转换计算。(参见下文)

 

    5)$center

    配合$geoWithin查询语句,使用$center指定圆心和半径,返回此区域内的数据,$center仅适用于传统的坐标对(2d),不查询GeoJSON对象。需要对查询字段建立2d索引,且不支持2dsphere索引。

{
   <location field>: {
      $geoWithin: { $center: [ [ <x>, <y> ] , <radius> ] }
   }
}

    radius(半径)的数值要根据你的坐标系的测量单位而定。如果你的坐标对,只是简单的平面坐标(自定义坐标系),那么radius就根据你自己的坐标系单位而定;如果是经纬度坐标对,那么radius就需要用实际的距离(米)与地球半径距离做一些换算。

    sharding collection中不支持$near和$nearSphere,请使用$geoNear替代,使用方式与它们相同。

    6)$centerSphere

    配合$geoWithin查询语句,基本原理同$center,只不过它适用于GeoJSON表示的地理位置数据(sphere,球面),半径以radians计量。此指令支持2d和2dsphere索引。

{
   <location field>: {
      $geoWithin: { $centerSphere: [ [ <x>, <y> ], <radius> ] }
   }
}

    比如查询经纬度为88W、30N的点,10英里范围内的空间数据,则可以使用如下查询语句(3963.2为地球半径的近似英里数):

db.places.find( {
  loc: { $geoWithin: { $centerSphere: [ [ -88, 30 ], 10/3963.2 ] } }
} )

    关于distance与radians的换算方式,【参见此文】

    7)$box

    配合$geoWithin使用,指定一个方形,查询此区域内的地理位置数据,像$center一样只支持普通的坐标对,不支持对GeoJSON对象的查询;$box需要指定“左下角”和“右上角”的坐标位置。只有2d索引支持$box,2dsphere不支持。

{
  <location field>: {
     $geoWithin: {
        $box: [
          [ <bottom left coordinates> ],
          [ <upper right coordinates> ]
        ]
     }
  }
}

    8)$polygon

    同$box,语法如下:

{
   <location field>: {
      $geoWithin: {
         $polygon: [ [ <x1> , <y1> ], [ <x2> , <y2> ], [ <x3> , <y3> ], ... ]
      }
   }
}

    9)其他

    尽管2d索引支持对球面距离的查询,不过我们尽可能考虑使用2dsphere索引来替代,如果你的数据是原始的“经纬度”。(如果是自定义坐标系,那么则可以不考虑使用2dsphere)。2d索引支持在欧几里得平面上计算距离查询数据,2d索引也支持如下几个操作命令使用球面(spherical)几何的距离查询,不过通常我们认为2dsphere是查询球面数据的首要选择:$nearSphere、$centerSphere、$near、以及使用{spherical : true}选项的geoNear指令。上述三个查询使用radians来表示距离,其他的查询则不是,对于球面(spherical)查询正常工作,需要将distance转换成radians,或者使用自己的距离单位将radians转换成距离。

    距离转换成radians:将距离除以球面的半径(比如地球),使用相同的距离测量单位。

    radians转换成距离:radians乘以球面半径(比如地球)。地球的半径近似为3963.2英里或者6378.1千米。

    比如如下查询collection中以[-74,40.74]为圆心半径为100英里的区域内的数据:

db.places.find( { loc: { $geoWithin: { $centerSphere: [ [ -74, 40.74 ] ,100 / 3963.2 ] } } } )

 

    $geoNear是在sharding collection中使用,它不是一个普通的操作,而是属于pipleline聚合函数。我们稍后再介绍,请参见【geoNear】    

 

六、索引策略

    1、按需创建索引

    索引可以提高query的查询效率,当然也会增加write时带来的开支,增加内存、磁盘的使用。

    1)如果所有的(大多数)查询都使用了同一个字段,那么可以给此字段建立单一字段索引。

    2)如果有些query只使用了一个key,而其他的query则是此key与其他key的组合,那么创建一个组合索引则比创建一个或者多个单一索引更加高效,索引的“最左前缀”不仅可以支持单一key的查询,而且还能兼顾它们的组合查询,且还在一定程度上了减少了因多个索引带来的性能开销。比如索引{x: 1, y : 1}开支持 {x : 1} 和 {x : 1, y : 1}两个key的查询。

    2、为了提高查询效率,我们尽可能确保所有的索引数据均可以保存在内存中,这样可以避免索引数据从磁盘索引文件中读取。可以使用db.collection.totalIndexSize()来查看此collection中所有索引的大小。如果你有多个collection,那么索引的总尺寸应该为所有collection的尺寸之和。

    当然索引不可能总是被全部载入内存,毕竟索引的尺寸终究有可能远大于内存尺寸,比如当数据量巨大或者系统中创建了多个index。mongodb只会将最近使用的indexes或者“right-most”保存在内存中,这可以更加高效的使用索引。

    3、选择性(Selectivity)是判定一个索引筛选数据的能力,越高效的索引其选择性越高,即可以排除掉更多的不匹配数据,更少的数据遍历。比如文档中有一个字段status,这个status有2个合法值“new”、“processed”,如果在status字段上建立索引,那么此索引的选择性就比较低,因为它只能筛选掉一半数据,这也意味着即使索引生效,也需要遍历整个collection中一半的数据才能得到最终结果,事实上对status建立索引是没有必要的。

    4、对于sort操作,如果能利用索引的排序直接获去结果,这是性能最好的;不过如果query不能通过index获得sort的结果,那么mongodb只能自己在内存中重新排序,在没有使用索引情况下排序操作所占用内存不得超过32M,否则将中断排序。

    对于单字段索引,索引的顺序对sort操作并无影响,应为无论是正序还是倒序,mongodb都可以很简单的翻转,这不会对性能有任何影响。

    对于多字段的组合索引中的sort操作,排序字段必须和它们在index中声明的顺序保持一致,比如索引{a : 1, b : 1},将不支持sort( { b : 1, a : 1}),且排序的方向必须和index声明的保持一致,比如上述索引支持{a : 1, b : 1} 和 { a : -1, b : -1},但无法支持{a : 1, b : -1} 和 { a : -1, b : 1}。

    如果参与排序的字段(和顺序)和索引字段一样或者是索引的前缀字段,且查询条件中也包含索引的前缀字段,那么mongodb则可以使用索引进行排序,这是一种比较高效的做法。比如索引{ a : 1, b : 1, c : 1},如下查询均可以利用索引排序:

db.data.find().sort( { a: 1 } )
db.data.find().sort( { a: -1 } )
db.data.find().sort( { a: 1, b: 1 } )
db.data.find().sort( { a: -1, b: -1 } )
db.data.find().sort( { a: 1, b: 1, c: 1 } )
db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } )

    如果参与排序的字段不是索引的前缀字段,具体是否能够利用索引排序,就要求查询条件的字段需要为索引的最左前缀字段,同时查询条件的字段和sort字段的也应该构成组合索引的左前缀,比如索引 { a : 1, b : 1, c: 1},那么此索引将支持如下几种排序方式:

db.data.find( { a: 5 } ).sort( { b: 1, c: 1 } )
db.data.find( { b: 3, a: 4 } ).sort( { c: 1 } )
db.data.find( { a: 5, b: { $lt: 3} } ).sort( { b: 1 } )

    如果查询字段和sort字段不能构成组合索引的左前缀,那么将不能高效的利用索引排序,或者有可能不会使用此索引,比如: 

db.data.find( { a: { $gt: 2 } } ).sort( { c: 1 } )
db.data.find( { c: 5 } ).sort( { c: 1 } )

 

七、geoNear实例

    如果collection中geo数据很大,我们需要考虑使用sharding架构设计,在sharding collection中,因为不支持$near、$nearSphere指令,事实上这两个指令反而是应用中最常用的操作,那么我们只能使用$geoNear来替代,用来查询“离指定Point一定距离内的geo数据”;$geoNear是一个聚合方法(Aggregation),不过它比较特殊,聚合方法的pipeline中通常只需要$geoNear一个stage即可,$geoNear这个stage中已经集成了“$match”、“$sort”、“$limit”三个功能,即不再需要在pipeline中额外声明$match、$sort、$limit三个stage。(详细内容,请参考【Aggregation】

    $geoNear操作接受一个查询document,此document中支持如下Filed:

    1)spherical:是否为“球面”,默认为false。如果使用2dsphere索引,那么它必须为true,则距离计算使用“米”(meters),参与查询的Point为GeoJSON类型,通常为地理位置的经纬度;如果使用2d索引,那么它为false,参与查询的Point为普通的坐标对,那么距离计算则使用radians,上文中已经提到radians与“米”之间的换算方式。

    2)limit或者num:这两个字段含义一致,表示返回数据的总条数。作用同stage $limit。

    3)maxDistance:可选值,表示离指定Point最大的距离,对于GeoJSON而言,distance的值为“米”,如果是普通坐标对,则为radians。

    4)minDistance:可选值,指离指定Point最小的距离。(3.2+版本)

    5)query:可选值,筛选文档的查询条件,同stage $match。

    6)distanceField:必填值,在输出的结果文档中增加一个字段,用来表示计算的距离,即location与指定的Point之间的距离。

    7)distanceMultiplier:可选值,数字类型,此值将会与“计算的距离”相乘,通常用来将距离转换,在2d数据、传统坐标对中使用(即spherical : false)。比如在2d数据中,计算的距离为radians,可以指定distanceMultiplier为地球的半径,那么最终返回的“distanceFiled”值为radians * distanceMultiplier,此时得到的是值为“米”(或者千米,取决于distanceMultiplier的单位);这样结果就和2dsphere(即spherical : true)查询的结果一致了。

    8)near:必填值,格式为GeoJSON(2dsphere)或者普通的坐标对(2d),指定参与比较的Point。

 

    当使用$geoNear时需要考虑到:

    1)它必须为pipeline的首个stage。

    2)必须包含distanceField,此字段将会在结果中输出。

    3)我们发现,$geoNear没有指定那个字段和“near”比较,所以这硬性要求collection中有且只有一个字段是2dsphere或者2d索引。这样$geoNear就直接使用索引字段参与查询。

    

    如下为$geoNear的shell查询方式语法:

db.places.aggregate([
   {
     $geoNear: {
        near: { type: "Point", coordinates: [ -73.99279 , 40.719296 ] },
        distanceField: "dist.calculated",
        maxDistance: 2,
        query: { type: "public" },
        includeLocs: "dist.location",
        num: 5,
        spherical: true
     }
   }
])

 

    如下为一个完整的JAVA示例:

    1)创建索引

MongoClient mongoClient = new MongoClient("127.0.0.1",27017); //mongos
MongoDatabase db = mongoClient.getDatabase("test");
MongoCollection<Document> collection = db.getCollection("geo_sample");
collection.createIndex(new Document("location","2dsphere"));

 

    2)插入geo数据

//insert some test datas;
List<Document> locations = new ArrayList<Document>();
locations.add(new Document("type","public")
            .append("location",new Point(new Position(-73.99279,40.719296))));
locations.add(new Document("type","public")
            .append("location",new Point(new Position(-73.99378,40.719396))));
locations.add(new Document("type","public")
            .append("location",new Point(new Position(-73.99379,40.719395))));
collection.insertMany(locations);

 

   3)聚合方法查询

//查询
List<Document> pipeline = new ArrayList<Document>();
Document geoNear = new Document();
Document content = new Document();
content.append("near",new Point(new Position(-73.99279,40.719296)));
content.append("distanceField","distance");
content.append("maxDistance",100);
content.append("query",new Document("type","public"));
content.append("spherical",true);
content.append("limit",20);
geoNear.append("$geoNear", content);

pipeline.add(geoNear);
MongoCursor<Document> cursor =  collection.aggregate(pipeline).allowDiskUse(true)
            .batchSize(128)
            .maxTime(60,TimeUnit.SECONDS)
            .useCursor(true).iterator();
while (cursor.hasNext()) {
   Document document = cursor.next();
   System.out.println(document.toJson());
}
cursor.close();

 

    4)查询结果样例

{
    "_id": {
        "$oid": "563ad23af4f64b0d42eadae4"
    }, 
    "type": "public", 
    "location": {
        "type": "Point", 
        "coordinates": [
            -73.99279, 
            40.719296
        ]
    }, 
    "distance": 0
}
{
    "_id": {
        "$oid": "563ad28df4f64b0d45c9780a"
    }, 
    "type": "public", 
    "location": {
        "type": "Point", 
        "coordinates": [
            -73.99378, 
            40.719396
        ]
    }, 
    "distance": 84.26495157209176
}
{
    "_id": {
        "$oid": "563ad28df4f64b0d45c9780b"
    }, 
    "type": "public", 
    "location": {
        "type": "Point", 
        "coordinates": [
            -73.99379, 
            40.719395
        ]
    }, 
    "distance": 85.08684215614285
}

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics