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 = &quot;place&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 = &quot;place&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 = &quot;place&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
|
>
new
Date()
ISODate("2013-02-26T09:20:31.203Z")
>
var
lon,lat,place;
>
for
(
var
i=0;i<1250000;i++){
...
// case 1 : [+,+]
/// 省略插入数据的脚本
... }
>
new
Date()
ISODate("2013-02-26T09:32:13.765Z")
> db.places.count()
5000000
|
从结果看,插入500w条简单数据只用了不到12分钟,占用硬盘情况如下:
1
2
3
4
5
6
7
8
9
10
11
|
2013-02-26 17:28 <DIR> .
2013-02-26 17:28 <DIR> ..
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 <DIR> _tmp
|
将近1G。
数据插入之后,就可以进行查询性能的测试了。
但是输入查询语句后,会发现很悲剧的报错:
1
2
3
4
5
6
|
> db.places.find( { loc : { $near : [50,50] } } ).limit(20)
error: {
"$err" : "can't find special index: 2d
for
: { loc: { $near: [ 50.0, 50.0
] } }",
"code" : 13038
}
|
对,如同error log说的那样,没有索引,加一个就行了。
在传统的RDBMS里加索引是很耗时的一件事情,mongodb怎样呢?
1
2
3
4
5
|
>
new
Date()
ISODate("2013-02-26T09:34:18.250Z")
> db.places.ensureIndex( { loc : "2d" } )
>
new
Date()
ISODate("2013-02-26T09:35:18.718Z")
|
从上面的shell执行情况及下面的server log
1
2
3
4
5
6
7
8
9
|
Tue Feb 26 17:34:18 [conn1] build index test.places { loc: "2d" }
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
|
>
new
Date()
ISODate("2013-02-26T09:36:34.531Z")
> db.places.find( { loc : { $near : [50,50] } } ).limit(2)
{ "_id" : ObjectId("512c7fee7e8b49a3221bedbb"), "place" : "place41258", "loc" :
[ 50.031758721131666, 49.982331061090065 ] }
{ "_id" : ObjectId("512c7fbc7e8b49a32216b807"), "place" : "place817715", "loc" :
[ 50.06532525944082, 49.949797918900195 ] }
>
new
Date()
ISODate("2013-02-26T09:36:34.546Z")
|
也可以用$maxDistance来限定更细致的条件:
1
2
3
4
5
6
7
8
9
|
>
new
Date()
ISODate("2013-02-26T09:37:26.656Z")
> db.places.find( { loc : { $near : [50,50] , $maxDistance : 5 } } ).limit(2)
{ "_id" : ObjectId("512c7fee7e8b49a3221bedbb"), "place" : "place41258", "loc" :
[ 50.031758721131666, 49.982331061090065 ] }
{ "_id" : ObjectId("512c7fbc7e8b49a32216b807"), "place" : "place817715", "loc" :
[ 50.06532525944082, 49.949797918900195 ] }
>
new
Date()
ISODate("2013-02-26T09:37:26.656Z")
|
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
|
>
new
Date()
ISODate("2013-02-26T09:37:59.375Z")
> db.runCommand( { geoNear : "places" , near : [50,50], num : 2 } );
{
"ns" : "test.places",
"near" : "1100110000001111110000001111110000001111110000001111",
"results" : [
{
"dis" : 0.03634291911943742,
"obj" : {
"_id" : ObjectId("512c7fee7e8b49a3221bedbb"),
"place" : "place41258",
"loc" : [
50.031758721131666,
49.982331061090065
]
}
},
{
"dis" : 0.08238712561900459,
"obj" : {
"_id" : ObjectId("512c7fbc7e8b49a32216b807"),
"place" : "place817715",
"loc" : [
50.06532525944082,
49.949797918900195
]
}
}
],
"stats" : {
"time" : 0,
"btreelocs" : 0,
"nscanned" : 164,
"objectsLoaded" : 48,
"avgDistance" : 0.059365022369221004,
"maxDistance" : 0.08240269999298451
},
"ok" : 1
}
>
new
Date()
ISODate("2013-02-26T09:37:59.406Z")
>
|
5. 根据几何边界来查询
mongodb还根据指定几何形状来限定查询,比如,给定一个矩形或圆形,来查询在这个范围内的记录。这通过使用$within参数来实现:
1
2
3
4
5
6
7
8
9
10
11
12
|
>
new
Date()
ISODate("2013-02-26T09:40:00.750Z")
> box = [[49.83083, -50.99756], [49.741404, -20.988135]]
[ [ 49.83083, -50.99756 ], [ 49.741404, -20.988135 ] ]
> db.places.find({"loc" : {"$within" : {"$box" : box}}}).limit(2)
{ "_id" : ObjectId("512c80077e8b49a3221ea1b0"), "place" : "place749764", "loc" :
[ 49.770283370229365, -36.326458739346215 ] }
{ "_id" : ObjectId("512c81197e8b49a3223d9cb4"), "place" : "place738746", "loc" :
[ 49.764167401092806, -34.02445155224063 ] }
>
new
Date()
ISODate("2013-02-26T09:40:00.765Z")
>
|
当然,除了矩形,也可以根据给定的圆心和半径来查询圆形范围内的点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
</p>
<p>&gt; center = [50, 50]
[ 50, 50 ]
&gt; radius = 10
10
&gt;
new
Date()
ISODate(&quot;2013-02-26T09:40:35.875Z&quot;)
&gt; .places.find({&quot;loc&quot; : {&quot;$within&quot; : {&quot;$center&quot; : [center, radius]}}}).limit(2)
{ &quot;_id&quot; : ObjectId(&quot;512c7f117e8b49a322049d6b&quot;), &quot;place&quot; : &quot;place332367&quot;, &quot;loc&quot; :
[ 50.62124690117222, 49.91787809141207 ] }
{ &quot;_id&quot; : ObjectId(&quot;512c80fc7e8b49a3223a72a3&quot;), &quot;place&quot; : &quot;place905935&quot;, &quot;loc&quot; :
[ 50.359744879419424, 47.486323341127715 ] }
&gt;
new
Date()
ISODate(&quot;2013-02-26T09:40:35.890Z&quot;)
&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/