两个坐标之间的距离,我该如何简化和/或使用不同的技术?

时间:2011-01-19 22:10:44

标签: sql mysql coordinates geospatial distance

我需要编写一个查询,允许我从提供的位置查找范围内的所有位置(英里)。

表格如下:

id  |  name  |  lat  |  lng 

所以我一直在做研究,发现:this my sql presentation

我已经在一张大约有100行的桌子上测试了它,还有更多! - 必须可扩展。

我首先尝试了更简单的事情:

//just some test data this would be required by user input    
set @orig_lat=55.857807; set @orig_lng=-4.242511; set @dist=10;

SELECT *, 3956 * 2 * ASIN(
          SQRT( POWER(SIN((orig.lat - abs(dest.lat)) * pi()/180 / 2), 2) 
              + COS(orig.lat * pi()/180 ) * COS(abs(dest.lat) * pi()/180)  
              * POWER(SIN((orig.lng - dest.lng) * pi()/180 / 2), 2) )) 
          AS distance
  FROM locations dest, locations orig
 WHERE orig.id = '1'
HAVING distance < 1
 ORDER BY distance;

此行返回 50ms ,这非常好! 然而,随着行的增加,这会显着减慢。

EXPLAIN显示它只使用显而易见的PRIMARY键。


然后阅读文章linked above。我试过这样的事情:

// defining variables - this when made into a stored procedure will call
// the values with a SELECT query.
set @mylon = -4.242511;
set @mylat = 55.857807;
set @dist = 0.5;

-- calculate lon and lat for the rectangle:
set @lon1 = @mylon-@dist/abs(cos(radians(@mylat))*69);
set @lon2 = @mylon+@dist/abs(cos(radians(@mylat))*69);
set @lat1 = @mylat-(@dist/69); 
set @lat2 = @mylat+(@dist/69);

-- run the query:

SELECT *, 3956 * 2 * ASIN(
          SQRT( POWER(SIN((@mylat - abs(dest.lat)) * pi()/180 / 2) ,2)
              + COS(@mylat * pi()/180 ) * COS(abs(dest.lat) * pi()/180)
              * POWER(SIN((@mylon - dest.lng) * pi()/180 / 2), 2) ))
          AS distance
  FROM locations dest
 WHERE dest.lng BETWEEN @lon1 AND @lon2
   AND dest.lat BETWEEN @lat1 AND @lat2
HAVING distance < @dist
 ORDER BY distance;

此查询的时间大约是 240ms ,这不是太糟糕,但比上一次慢。但我可以想象,在更高的行数下,这会更快。但是,EXPLAIN会将可能的密钥显示为latlngPRIMARY并使用PRIMARY

我怎样才能做得更好???

我知道我可以将lat lng存储为POINT();但是我也没有找到太多的文件来证明它是否更快或更准确?

任何其他想法都会被愉快地接受!

非常感谢!

-Stefan


更新

Jonathan Leffler指出我犯了一些我没有注意到的错误:

我只将abs()放在其中一个lat值上。当我没有必要时,我也在第二个WHERE子句中使用id搜索。在第一个查询纯粹是实验性的,第二个查询更有可能达到生产。

这些更改后EXPLAIN显示密钥现在使用lng列,平均时间响应 180ms ,这是一项改进。

5 个答案:

答案 0 :(得分:2)

任何其他想法都会被高兴地接受!

如果您想要速度(和简单性),您需要从数据库获得一些不错的地理空间支持。这引入了地理空间数据类型,地理空间索引和(许多)处理/构建/分析地理空间数据的功能。

MySQL implements a part of the OpenGIS specifications虽然它是(上次我检查过的)非常粗糙的边缘/过早(对任何实际工作都没用)。

PostGis上的{p> PostgreSql会使这个简单易读:

(这会找到tableb中距离距离表格中a点123的a点近1000米的所有点)

select 
    myvalue
from 
    tablea, tableb
where 
    st_dwithin(tablea.the_geom, tableb.the_geom, 1000)
and
    tablea.id = 123

答案 1 :(得分:2)

第一个查询忽略您设置的参数 - 使用1代替@dist作为距离,并使用表别名orig代替参数@orig_lat@orig_lon

然后你可以在表和它自己之间进行笛卡尔积的查询,如果可以避免它,这很少是个好主意。由于过滤条件orig.id = 1,您可以使用它,这意味着orig中只有一行与dest中的每一行相连(包括dest.id = 1的点你应该有条件AND orig.id != dest.id)。您还有一个HAVING子句但没有GROUP BY子句,这表示存在问题。 HAVING子句不与任何聚合相关,但HAVING子句(主要)用于比较聚合值。

除非我的记忆失败,否则COS(ABS(x))=== COS(x),所以你可以通过放弃ABS()来简化事情。如果不这样做,不清楚为什么一个纬度需要ABS而另一个纬度不需要 - 对称性在球面三角学方面至关重要。

你有一定数量的幻数 - 值69可能是一定程度(经度,赤道)的英里数,而3956是地球的半径。

如果给定位置接近极点,我怀疑计算的方框。在极端情况下,您可能需要允许任何经度。

第二个查询中的条件dest.id = 1是奇数;我认为它应该被省略,但它的存在应该加快速度,因为只有一行符合这个条件。因此,额外的时间令人费解。但是使用主键索引是合适的。

您应该将HAVING子句中的条件移动到WHERE子句中。

但我不确定这真的有帮助......

答案 2 :(得分:1)

NGS在线逆向测地计算器是计算地球椭球上任意两个位置之间距离的传统参考方法:

http://www.ngs.noaa.gov/cgi-bin/Inv_Fwd/inverse2.prl

但是上面的计算器仍然存在问题。特别是在两个近对映位置之间,计算出的距离可以显示几十公里的误差! Thaddeus Vincenty很久以前就发现了数字问题的根源(第92页):

http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf

无论如何,最好使用Charles Karney提供的可靠且非常准确的在线计算器:

http://geographiclib.sourceforge.net/cgi-bin/Geod

答案 3 :(得分:0)

关于提高绩效的一些想法。它不会从可维护性的角度简化事情(使事情变得更复杂),但它可以帮助实现可伸缩性。

  1. 由于您知道半径,因此可以为边界框添加条件,这可能允许db优化查询以消除某些行而无需执行trig计算。

  2. 您可以预先计算存储位置的lat / lon的某些trig值,并将它们存储在表中。这会在插入记录时改变一些性能成本,但如果查询数量超过插入,这将是好的。请参阅此答案以了解此方法:

    Query to get records based on Radius in SQLite?

  3. 您可以查看类似geohashing的内容。

  4.   

    在数据库中使用时,地理数据的结构有两个优点。其次,这个索引结构可以用于快速和肮脏的邻近搜索 - 最近的点通常是最接近的地理位置。

    您可以搜索SO以获取有关如何实施的一些想法:   https://stackoverflow.com/search?q=geohash

答案 4 :(得分:0)

如果您只对相当小的距离感兴趣,可以用矩形网格近似地理网格。

SELECT *, SQRT(POWER(RADIANS(@mylat - dest.lat), 2) +
               POWER(RADIANS(@mylon - dst.lng)*COS(RADIANS(@mylat)), 2)
              )*@radiusOfEarth AS approximateDistance
…

您可以通过在数据库中存储弧度而不是(或除了)度数来提高效率。如果您的查询可能穿过180°子午线,那么需要额外注意,但许多应用程序不必处理这些位置。您还可以尝试将POWER( x )更改为 x * x ,这可能会获得计算得更快。