MongoDb 物理位置应用实现
1代码实现
官方驱动2.7版本
1.1范围查找
/// <summary> /// 半径范围查找位置信息 /// </summary> /// <param name="x">经度</param> /// <param name="y">纬度</param> /// <param name="radius">半径</param> /// <returns></returns> public async Task<List<T>> GeoWithinCenterSphere(double x,double y,double radius) { var filter = Builders<T>.Filter.GeoWithinCenterSphere("Location", x, y, radius / 6378.1); return await collection.Find(filter).ToListAsync(); }
6378.1貌似是公里
1.2范围查找并返回距离 官方文档
/*附加距离 result里的dis为距离*/ db.runCommand( { geoNear: "Event", /*Collecti名*/ near: { type: "Point", coordinates: [ 114.24791, 30.598503 ] }, spherical: true, maxDistance:5000 /* 单位为米 */ } )
2数据库准备
2.1创建索引 详情官方文档
db.Event.ensureIndex({'Location':'2dsphere'})
2.2数据查找 官方文档
db.Event.find( { Location: { $geoWithin: { $centerSphere: [ [ 114.24791, 30.598503 ] ,10/6378.1 ] } } } )
10/6378.1=10公里 有个
2.3附带距离的查找
/*附加距离 result里的dis为距离*/ db.runCommand( { geoNear: "Event", /*Collecti名*/ near: { type: "Point", coordinates: [ 114.24791, 30.598503 ] }, spherical: true, maxDistance:5000 /* 单位为米 */ } )
3Mongodb索引介绍
转自 http://shift-alt-ctrl.iteye.com/blog/2250101
地理空间索引(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
- }
参考:https://my.oschina.net/manmao/blog/741628