mongodb中地理位置信息类型及性能初探

1. 问题的引出
前两天说到LBS系统里如何找到附近的点,假如数据库存储如下信息{x,y}分表表示经度和纬度(这里统统存为正数,方便计算),那么如果取得当前位置信息(x1,y1)如何去数据库里查附近的信息(记录)呢?最原始的想法差不多是:

1
2
3
4
5
select *
from tbl
where abc
order by (x1 - x ) ^2 + (y1 - y)^2
limit 10

很显然,这样的话数据库性能会很差的。
那好,根据每个点的信息,作出一个位置指数
loc_index = (x + 360 ) + y
这样,loc_index的值的范围应该在360 到 360 + 360 + 180之间,
然后再选择的时候计算出{x1,y1}的loc_index1,在从数据库里找到相同的值就行了,或者按差值排列。

缺点:计算量,复杂,不好统计距离,山寨,不官方,非权威,不能统计圆形区域,等等。也许存在逻辑错误,但这只是一种假设,因为这不是要确实去实现它,只是失眠想到的一件事情。

2. mongodb的情况

然后第二天,想到了一些NoSql支持这种geo类型,甚至包括PostgreSql都支持了。。
那就拿来试试吧,最简单的就是mongodb了。

本来想用1000w条数据试的,但是可能和OS有关,插入到600w条的时候就死了,再启动出错,repair也不行,最后采用了500w条数据做实验。
数据类型很简单,{place:”some text”,loc : [ -5.9282, 23.222 ] }

插入数据的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
db.places.remove()
new Date()
var lon,lat,place;
for ( var i=0;i<1250000;i++){
   // case 1 : [+,+]
   lon = 179 * Math.random();
   lat = 179 * Math.random();
   place = "place" + Math.round(Math.random()*1000000);
   p = { place: place ,loc : [ lon, lat ] };
   db.places.save(p);</p>
 
<p> // case 2 : [+,-]
   lon = 179 * Math.random();
   lat = -179 * Math.random();
   place = &amp;quot;place&amp;quot; + Math.round(Math.random()*1000000);
   p = { place: place ,loc : [ lon, lat ] };
   db.places.save(p);</p>
 
<p> // case 3 : [-,+]
   lon = -179 * Math.random();
   lat = 179 * Math.random();
   place = &amp;quot;place&amp;quot; + Math.round(Math.random()*1000000);
   p = { place: place ,loc : [ lon, lat ] };
   db.places.save(p);</p>
 
<p> // case 4 : [-,-]
   lon = -179 * Math.random();
   lat = -179 * Math.random();
   place = &amp;quot;place&amp;quot; + Math.round(Math.random()*1000000);
   p = { place: place ,loc : [ lon, lat ] };
   db.places.save(p);
}
new Date()

以下是实行结果:

1
2
3
4
5
6
7
8
9
10
11
&gt; new Date()
ISODate(&quot;2013-02-26T09:20:31.203Z&quot;)
&gt; var lon,lat,place;
&gt; for ( var i=0;i&lt;1250000;i++){
...   // case 1 : [+,+]
/// 省略插入数据的脚本
... }
&gt; new Date()
ISODate(&quot;2013-02-26T09:32:13.765Z&quot;)
&gt; db.places.count()
5000000

从结果看,插入500w条简单数据只用了不到12分钟,占用硬盘情况如下:

1
2
3
4
5
6
7
8
9
10
11
2013-02-26  17:28    &lt;DIR&gt;          .
2013-02-26  17:28    &lt;DIR&gt;          ..
2013-02-26  17:19                 5 mongod.lock
2013-02-26  17:20        16,777,216 test.0
2013-02-26  17:20        33,554,432 test.1
2013-02-26  17:21        67,108,864 test.2
2013-02-26  17:22       134,217,728 test.3
2013-02-26  17:24       268,435,456 test.4
2013-02-26  17:28       536,608,768 test.5
2013-02-26  17:20        16,777,216 test.ns
2013-02-26  17:20    &lt;DIR&gt;          _tmp

将近1G。
数据插入之后,就可以进行查询性能的测试了。
但是输入查询语句后,会发现很悲剧的报错:

1
2
3
4
5
6
&gt; db.places.find( { loc : { $near : [50,50] } } ).limit(20)
error: {
         &quot;$err&quot; : &quot;can't find special index: 2d for : { loc: { $near: [ 50.0, 50.0
  ] } }&quot;,
         &quot;code&quot; : 13038
}

对,如同error log说的那样,没有索引,加一个就行了。
在传统的RDBMS里加索引是很耗时的一件事情,mongodb怎样呢?

1
2
3
4
5
&gt; new Date()
ISODate(&quot;2013-02-26T09:34:18.250Z&quot;)
&gt; db.places.ensureIndex( { loc : &quot;2d&quot; } )
&gt; new Date()
ISODate(&quot;2013-02-26T09:35:18.718Z&quot;)

从上面的shell执行情况及下面的server log

1
2
3
4
5
6
7
8
9
Tue Feb 26 17:34:18 [conn1] build index test.places { loc: &quot;2d&quot; }
                 2000000/5000000 40%
                 4000000/5000000 80%
Tue Feb 26 17:34:55 [conn1]      external sort used : 5 files  in 37 secs
                 2114300/5000000 42%
                 4396000/5000000 87%
Tue Feb 26 17:35:18 [conn1]      done building bottom layer, going to commit
Tue Feb 26 17:35:18 [conn1] build index done 5000000 records 60.453 secs
Tue Feb 26 17:35:18 [conn1] insert test.system.indexes 60453ms

可以看出,加索引过程只用了1分钟左右,效率也是能让人接受的。
接下来,就可以开始测试下查询的性能了。(下面的查询前后都有日期输出,用了确认查询时间)

3. 用$near来查询附近记录
$near用了查找里给定地点最近的记录,并按距离从小到大排列,可以用limit来限定返回记录个数:

1
2
3
4
5
6
7
8
9
&gt; new Date()
ISODate(&quot;2013-02-26T09:36:34.531Z&quot;)
&gt; db.places.find( { loc : { $near : [50,50] } } ).limit(2)
{ &quot;_id&quot; : ObjectId(&quot;512c7fee7e8b49a3221bedbb&quot;), &quot;place&quot; : &quot;place41258&quot;, &quot;loc&quot; :
[ 50.031758721131666, 49.982331061090065 ] }
{ &quot;_id&quot; : ObjectId(&quot;512c7fbc7e8b49a32216b807&quot;), &quot;place&quot; : &quot;place817715&quot;, &quot;loc&quot; :
  [ 50.06532525944082, 49.949797918900195 ] }
&gt; new Date()
ISODate(&quot;2013-02-26T09:36:34.546Z&quot;)

也可以用$maxDistance来限定更细致的条件:

1
2
3
4
5
6
7
8
9
&gt; new Date()
ISODate(&quot;2013-02-26T09:37:26.656Z&quot;)
&gt; db.places.find( { loc : { $near : [50,50] , $maxDistance : 5 } } ).limit(2)
{ &quot;_id&quot; : ObjectId(&quot;512c7fee7e8b49a3221bedbb&quot;), &quot;place&quot; : &quot;place41258&quot;, &quot;loc&quot; :
[ 50.031758721131666, 49.982331061090065 ] }
{ &quot;_id&quot; : ObjectId(&quot;512c7fbc7e8b49a32216b807&quot;), &quot;place&quot; : &quot;place817715&quot;, &quot;loc&quot; :
  [ 50.06532525944082, 49.949797918900195 ] }
&gt; new Date()
ISODate(&quot;2013-02-26T09:37:26.656Z&quot;)

4. 用geoNear命令
除了用dbfind查询语句,还可以用geoNear命令来执行上面类似的查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
&gt; new Date()
ISODate(&quot;2013-02-26T09:37:59.375Z&quot;)
&gt; db.runCommand( { geoNear : &quot;places&quot; , near : [50,50], num : 2 } );
{
         &quot;ns&quot; : &quot;test.places&quot;,
         &quot;near&quot; : &quot;1100110000001111110000001111110000001111110000001111&quot;,
         &quot;results&quot; : [
                 {
                         &quot;dis&quot; : 0.03634291911943742,
                         &quot;obj&quot; : {
                                 &quot;_id&quot; : ObjectId(&quot;512c7fee7e8b49a3221bedbb&quot;),
                                 &quot;place&quot; : &quot;place41258&quot;,
                                 &quot;loc&quot; : [
                                         50.031758721131666,
                                         49.982331061090065
                                 ]
                         }
                 },
                 {
                         &quot;dis&quot; : 0.08238712561900459,
                         &quot;obj&quot; : {
                                 &quot;_id&quot; : ObjectId(&quot;512c7fbc7e8b49a32216b807&quot;),
                                 &quot;place&quot; : &quot;place817715&quot;,
                                 &quot;loc&quot; : [
                                         50.06532525944082,
                                         49.949797918900195
                                 ]
                         }
                 }
         ],
         &quot;stats&quot; : {
                 &quot;time&quot; : 0,
                 &quot;btreelocs&quot; : 0,
                 &quot;nscanned&quot; : 164,
                 &quot;objectsLoaded&quot; : 48,
                 &quot;avgDistance&quot; : 0.059365022369221004,
                 &quot;maxDistance&quot; : 0.08240269999298451
         },
         &quot;ok&quot; : 1
}
&gt; new Date()
ISODate(&quot;2013-02-26T09:37:59.406Z&quot;)
&gt;

5. 根据几何边界来查询
mongodb还根据指定几何形状来限定查询,比如,给定一个矩形或圆形,来查询在这个范围内的记录。这通过使用$within参数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
&gt; new Date()
ISODate(&quot;2013-02-26T09:40:00.750Z&quot;)
&gt; box = [[49.83083, -50.99756], [49.741404, -20.988135]]
[ [ 49.83083, -50.99756 ], [ 49.741404, -20.988135 ] ]
&gt; db.places.find({&quot;loc&quot; : {&quot;$within&quot; : {&quot;$box&quot; : box}}}).limit(2)
{ &quot;_id&quot; : ObjectId(&quot;512c80077e8b49a3221ea1b0&quot;), &quot;place&quot; : &quot;place749764&quot;, &quot;loc&quot; :
  [ 49.770283370229365, -36.326458739346215 ] }
{ &quot;_id&quot; : ObjectId(&quot;512c81197e8b49a3223d9cb4&quot;), &quot;place&quot; : &quot;place738746&quot;, &quot;loc&quot; :
  [ 49.764167401092806, -34.02445155224063 ] }
&gt; new Date()
ISODate(&quot;2013-02-26T09:40:00.765Z&quot;)
&gt;

当然,除了矩形,也可以根据给定的圆心和半径来查询圆形范围内的点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</p>
 
<p>&amp;gt; center = [50, 50]
[ 50, 50 ]
&amp;gt; radius = 10
10
&amp;gt; new Date()
ISODate(&amp;quot;2013-02-26T09:40:35.875Z&amp;quot;)
&amp;gt; .places.find({&amp;quot;loc&amp;quot; : {&amp;quot;$within&amp;quot; : {&amp;quot;$center&amp;quot; : [center, radius]}}}).limit(2)
{ &amp;quot;_id&amp;quot; : ObjectId(&amp;quot;512c7f117e8b49a322049d6b&amp;quot;), &amp;quot;place&amp;quot; : &amp;quot;place332367&amp;quot;, &amp;quot;loc&amp;quot; :
  [ 50.62124690117222, 49.91787809141207 ] }
{ &amp;quot;_id&amp;quot; : ObjectId(&amp;quot;512c80fc7e8b49a3223a72a3&amp;quot;), &amp;quot;place&amp;quot; : &amp;quot;place905935&amp;quot;, &amp;quot;loc&amp;quot; :
  [ 50.359744879419424, 47.486323341127715 ] }
&amp;gt; new Date()
ISODate(&amp;quot;2013-02-26T09:40:35.890Z&amp;quot;)
&amp;gt;

总结一下,mongodb对geo类型的支持还是很全面很方便的,最关键的是性能是完全可以接受的。
注:
系统测试环境
Dell OptiPlex 360
Win XP Service Pack 3
CPU 2.6G
内存 3G


转自:http://liubin.org/2013/02/26/geo_data_type_and_performance_in_mongodb/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值