查找两个纬度/经度点之间距离的最快方法


227

目前,我在mysql数据库中的位置不足一百万,所有位置都包含经度和纬度信息。

我试图通过查询找到一个点和许多其他点之间的距离。它并没有我想要的那么快,特别是每秒100次以上的命中。

是否有更快的查询,或者可能是比mysql更快的系统?我正在使用此查询:

SELECT 
  name, 
   ( 3959 * acos( cos( radians(42.290763) ) * cos( radians( locations.lat ) ) 
   * cos( radians(locations.lng) - radians(-71.35368)) + sin(radians(42.290763)) 
   * sin( radians(locations.lat)))) AS distance 
FROM locations 
WHERE active = 1 
HAVING distance < 10 
ORDER BY distance;

注意:提供的距离以英里为单位。如果您需要公里,请使用6371代替3959


31
您提供的公式似乎包含许多恒定的元素。是否可以预先计算数据并将这些值存储在数据库中?例如3959 * acos(cos(radians(42.290763))是一个常数,但其中有4个主要计算,而您只能存储6696.7837吗?
Peter M

1
还是至少在查询之外预先计算了常量?这将减少必须要做的工作。
Peter M

2
@Peter M似乎任何体面的SQL数据库都会优化,因此只计算一次。
mhenry1384

25
对于那些想知道的人,42.290763是纬度,-71.35368是要计算距离的点的经度。
user276648

14
仅供参考,该公式计算出的距离以英里为单位,而不是以千米为单位。请用3959替换为6371,以千米为单位获取结果
Sahil 2015年

Answers:


115
  • 使用表PointGeometry数据类型的值创建点MyISAM从Mysql 5.7.5开始,表现在InnoDB还支持SPATIAL索引。

  • SPATIAL在这些点上创建索引

  • 使用MBRContains()查找值:

    SELECT  *
    FROM    table
    WHERE   MBRContains(LineFromText(CONCAT(
            '('
            , @lon + 10 / ( 111.1 / cos(RADIANS(@lon)))
            , ' '
            , @lat + 10 / 111.1
            , ','
            , @lon - 10 / ( 111.1 / cos(RADIANS(@lat)))
            , ' '
            , @lat - 10 / 111.1 
            , ')' )
            ,mypoint)
    

MySQL 5.1以上:

    SELECT  *
    FROM    table
    WHERE   MBRContains
                    (
                    LineString
                            (
                            Point (
                                    @lon + 10 / ( 111.1 / COS(RADIANS(@lat))),
                                    @lat + 10 / 111.1
                                  ),
                            Point (
                                    @lon - 10 / ( 111.1 / COS(RADIANS(@lat))),
                                    @lat - 10 / 111.1
                                  ) 
                            ),
                    mypoint
                    )

这将在框中近似选择所有点(@lat +/- 10 km, @lon +/- 10km)

实际上,这不是一个盒子,而是一个球形矩形:球体的经度和纬度绑定段。这可能与弗朗兹·约瑟夫(Franz Joseph Land)上的普通矩形不同,但在大多数人居住的地方都非常接近矩形。

  • 应用其他过滤以选择圆内的所有内容(而不是正方形)

  • 可能应用其他精细过滤以解决大圆距(对于大距离)


15
@Quassnoi:进行了一些更正:您可能需要将坐标的顺序切换为长纬度。同样,纵向距离与纬度(而不是经度)的余弦成正比。你会想要将它从乘法变成除法,所以你的第一个坐标将被修正为@lon - 10 / ( 111.1 / cos(@lat))(并且是在对第二次的一切是正确的。
M.戴夫Auayan

8
警告:答案的正文尚未经过编辑以符合@M的非常有效的注释。戴夫·奥阿扬。进一步说明:如果感兴趣的圆(a)包括一个极点,或者(b)与经度的+/- 180度子午线相交,则此方法呈梨形。此外,cos(lon)仅在较小的距离上使用才是准确的。参见janmatuschek.de/LatitudeLongitudeBoundingCoordinates
John Machin 2010年

3
有什么方法可以让我们深入了解常量(10、111.11,@ lat,@ lon,mypoint)代表什么?我假设10代表千米距离,@ lat和@lon代表所提供的纬度和经度,但是在示例中111.11和mypoint代表什么?
ashays 2011年

4
@ashays:111.(1)纬度大约有公里。mypoint是表格中存储坐标的字段。
Quassnoi

1
另一个错误更正-您错过了倒数第二行的结尾)
ina

100

这不是MySql的特定答案,但是它将提高sql语句的性能。

您实际上正在做的是计算到表中每个点的距离,以查看该距离是否在给定点的10个单位内。

在运行此sql之前,您可以做的是创建四个点,这些点在侧面绘制20个单位的框,将点居中。(x1,y1)。。。(x4,y4),其中(x1,y1)是(给定的长度+ 10个单位,给定的纬度+ 10个单位)。。。(给定的长-10个单位,给定的-10个单位)。 实际上,您只需要两个点,左上角和右下角分别称为(X1,Y1)和(X2,Y2)

现在,您的SQL语句使用这些点来排除从给定点开始绝对超过10u的行,它可以使用纬度和经度上的索引,因此将比当前的速度快几个数量级。

例如

select . . . 
where locations.lat between X1 and X2 
and   locations.Long between y1 and y2;

盒子方法会返回假阳性(您可以在盒子的角上拾取距给定点> 10u的点),因此仍然需要计算每个点的距离。但是,这又会更快,因为您已将要测试的点数极大地限制为框中的点数。

我称这种技术为“思维在盒子里” :)

编辑:可以将其放入一个SQL语句吗?

我不知道mySql或Php的功能,抱歉。我不知道最好的地方是建立这四个点,或者如何将它们传递给Php中的mySql查询。但是,一旦您掌握了这四点,就不会阻止您将自己的SQL语句与我的SQL语句结合起来。

select name, 
       ( 3959 * acos( cos( radians(42.290763) ) 
              * cos( radians( locations.lat ) ) 
              * cos( radians( locations.lng ) - radians(-71.35368) ) 
              + sin( radians(42.290763) ) 
              * sin( radians( locations.lat ) ) ) ) AS distance 
from locations 
where active = 1 
and locations.lat between X1 and X2 
and locations.Long between y1 and y2
having distance < 10 ORDER BY distance;

我知道使用MS SQL可以构建一个声明四个浮点数(X1,Y1,X2,Y2)的SQL语句,并在“主”选择语句之前计算它们,就像我说的那样,我不知道是否可以使用MySQL的。但是,我仍然倾向于在C#中构建这四个点,并将它们作为参数传递给SQL查询。

抱歉,如果有人可以回答有关MySQL&Php的特定部分,请帮忙编辑此答案。


4
在此演示文稿中,您可以找到用于此方法的mysql过程:scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
Lucia 2010年

37
要按公里而不是英里进行搜索,请将3959替换为6371。– ErichBSchulz 2013
56

4
+1,不错的选择;添加框将我的查询从4s平均减少到0.03s平均。
jvenema

1
看起来很合逻辑,您为此解决方案保留了一个奖项!在具有200万记录的数据库上,查询时间从16秒变为0.06秒。 注意:如果从查询中删除距离计算并在程序代码中进行距离计算,则速度更快(对于大型表)!
NLAnaconda 2014年

2
@ Binary Worrier:因此,X1,X2和Y1,Y2分别是此处的示例:blog.fedecarg.com/2009/02/08/。请告知。
Prabhat

14

以下MySQL函数已发布在此博客文章中。我还没有做过很多测试,但是从我从帖子中收集的信息来看,如果您的纬度和经度字段已建立索引,那么这可能对您很有效:

DELIMITER $$

DROP FUNCTION IF EXISTS `get_distance_in_miles_between_geo_locations` $$
CREATE FUNCTION get_distance_in_miles_between_geo_locations(
  geo1_latitude decimal(10,6), geo1_longitude decimal(10,6), 
  geo2_latitude decimal(10,6), geo2_longitude decimal(10,6)) 
returns decimal(10,3) DETERMINISTIC
BEGIN
  return ((ACOS(SIN(geo1_latitude * PI() / 180) * SIN(geo2_latitude * PI() / 180) 
    + COS(geo1_latitude * PI() / 180) * COS(geo2_latitude * PI() / 180) 
    * COS((geo1_longitude - geo2_longitude) * PI() / 180)) * 180 / PI()) 
    * 60 * 1.1515);
END $$

DELIMITER ;

用法示例:

假设一个places带有latitude&字段的表longitude

SELECT get_distance_in_miles_between_geo_locations(-34.017330, 22.809500,
latitude, longitude) AS distance_from_input FROM places;

我已经尝试过了,并且效果很好,但是由于某种原因,它不允许我基于distance_from_input输入WHERE语句。知道为什么不吗?
克里斯·威瑟

您可以将其作为子选择:将* from(...)选为t,其中distance_from_input> 5;
布莱德·帕克斯

2
或直接进行以下操作:从get_distance_in_miles_between_geo_locations(-34.017330,22.809500,纬度,经度)> 5000的地方选择*;或者
布拉德·帕克斯

2
返回仪表:SELECT ROUND(((ACOS(SIN(lat1 * PI() / 180) * SIN(lat2 * PI() / 180) + COS(lat1 * PI() / 180) * COS(lat2 * PI() / 180) * COS((lnt1 - lnt2) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) * 1.609344 * 1000) AS distance
Mohammad

13

我需要解决类似的问题(按距单个点的距离来过滤行),并通过将原始问题与答案和注释结合起来,我想出了一个对MySQL 5.6和5.7都非常适用的解决方案。

SELECT 
    *,
    (6371 * ACOS(COS(RADIANS(56.946285)) * COS(RADIANS(Y(coordinates))) 
    * COS(RADIANS(X(coordinates)) - RADIANS(24.105078)) + SIN(RADIANS(56.946285))
    * SIN(RADIANS(Y(coordinates))))) AS distance
FROM places
WHERE MBRContains
    (
    LineString
        (
        Point (
            24.105078 + 15 / (111.320 * COS(RADIANS(56.946285))),
            56.946285 + 15 / 111.133
        ),
        Point (
            24.105078 - 15 / (111.320 * COS(RADIANS(56.946285))),
            56.946285 - 15 / 111.133
        )
    ),
    coordinates
    )
HAVING distance < 15
ORDER By distance

coordinates是具有类型的字段,POINT并且具有用于计算距离的SPATIAL索引
6371,以公里
56.946285为单位是中心点的纬度是中心点
24.105078的经度
15是以公里为单位的最大距离

在我的测试中,MySQL在coordinates字段上使用SPATIAL索引来快速选择矩形内的所有行,然后计算所有已过滤位置的实际距离,以排除矩形角中的位置,而仅将圆内的位置保留。

这是我的结果的可视化:

地图

灰色星标将地图上的所有点可视化,黄色星标是MySQL查询返回的点。矩形的角(但在圆的外部)内的灰星MBRContains()HAVING子句选中,然后被子句取消选中。


无法对此进行足够的投票。使用这种方法,在具有大约500万条记录和一个空间索引的表中进行搜索,在旧的A8处理器上的搜索时间为0.005秒。我知道可以将3371替换为6371,以英里为单位获取结果,但是111.133和111.320的值是否需要调整,或者它们是否普遍不变?
Wranorn

很好的解决方案。
SeaBiscuit

如何创建Point是POINT(lat,lng)还是POINT(lng,lat)
user606669

2
@ user606669它的POINT(LNG,LAT)
马里斯Kiseļovs

X()和Y()函数现在应该是ST_Y和ST_X。
Andreas

11

如果您使用的是MySQL 5.7。*,则可以使用st_distance_sphere(POINT,POINT)

Select st_distance_sphere(POINT(-2.997065, 53.404146 ), POINT(58.615349, 23.56676 ))/1000  as distcance

1
这是一个很好且易于阅读的替代方法。请记住,POINT()的参数顺序为(lng,lat),否则您将以“ close”结尾,但结果与此处的其他方法仍然非常不同。见:stackoverflow.com/questions/35939853/...
安迪·P

9
SELECT * FROM (SELECT *,(((acos(sin((43.6980168*pi()/180)) * 
sin((latitude*pi()/180))+cos((43.6980168*pi()/180)) * 
cos((latitude*pi()/180)) * cos(((7.266903899999988- longitude)* 
pi()/180))))*180/pi())*60*1.1515 ) as distance 
FROM wp_users WHERE 1 GROUP BY ID limit 0,10) as X 
ORDER BY ID DESC

这是MySQL中各点之间的距离计算查询,我在一个较长的数据库中使用了它,它工作得很好!注意:根据您的要求进行更改(数据库名称,表名称,列等)。


值1.1515代表什么?我以前看过类似的公式,但是它使用1.75而不是1.1515。
TryHarder '16

1
在回答我自己的问题时,我认为答案可能就在这里stackoverflow.com/a/389251/691053
TryHarder

8
set @latitude=53.754842;
set @longitude=-2.708077;
set @radius=20;

set @lng_min = @longitude - @radius/abs(cos(radians(@latitude))*69);
set @lng_max = @longitude + @radius/abs(cos(radians(@latitude))*69);
set @lat_min = @latitude - (@radius/69);
set @lat_max = @latitude + (@radius/69);

SELECT * FROM postcode
WHERE (longitude BETWEEN @lng_min AND @lng_max)
AND (latitude BETWEEN @lat_min and @lat_max);

资源


11
请引用您的消息来源。这是从:blog.fedecarg.com/2009/02/08/...
Redburn镇

在这种情况下69是多少?万一我们有地球半径怎么办?
CodeRunner

2
1 Latittude的公里为111公里。1 Latittude的英里为69英里。和69英里= 111公里。这就是为什么我们在转换中使用参数。
CodeRunner

我一直在寻找这个。不知道它可以这么简单。非常感谢。
Vikas

这会不正确吗,因为lng_min / lng_max将需要在半径数学中使用lat_min和lat_max?

6
   select
   (((acos(sin(('$latitude'*pi()/180)) * sin((`lat`*pi()/180))+cos(('$latitude'*pi()/180)) 
    * cos((`lat`*pi()/180)) * cos((('$longitude'- `lng`)*pi()/180))))*180/pi())*60*1.1515) 
    AS distance
    from table having distance<22;

5

一个MySQL函数,它返回两个坐标之间的米数:

CREATE FUNCTION DISTANCE_BETWEEN (lat1 DOUBLE, lon1 DOUBLE, lat2 DOUBLE, lon2 DOUBLE)
RETURNS DOUBLE DETERMINISTIC
RETURN ACOS( SIN(lat1*PI()/180)*SIN(lat2*PI()/180) + COS(lat1*PI()/180)*COS(lat2*PI()/180)*COS(lon2*PI()/180-lon1*PI()/180) ) * 6371000

要以其他格式返回值,请6371000在您选择的单位中将函数中的替换为地球半径。例如,公里为公里6371,英里为3959

要使用该函数,只需像在MySQL中的其他任何函数一样调用它即可。例如,如果您有一个表格city,则可以找到每个城市到每个其他城市之间的距离:

SELECT
    `city1`.`name`,
    `city2`.`name`,
    ROUND(DISTANCE_BETWEEN(`city1`.`latitude`, `city1`.`longitude`, `city2`.`latitude`, `city2`.`longitude`)) AS `distance`
FROM
    `city` AS `city1`
JOIN
    `city` AS `city2`

4

有关如何作为MySQL插件安装的详细信息的完整代码位于:https : //github.com/lucasepe/lib_mysqludf_haversine

我去年发布了此评论。由于好心@TylerCollier建议我发布作为答案,所以就在这里。

另一种方法是编写一个自定义UDF函数,该函数返回两点之间的正弦距离。该函数可以接受输入:

lat1 (real), lng1 (real), lat2 (real), lng2 (real), type (string - optinal - 'km', 'ft', 'mi')

所以我们可以这样写:

SELECT id, name FROM MY_PLACES WHERE haversine_distance(lat1, lng1, lat2, lng2) < 40;

以获取距离小于40公里的所有记录。要么:

SELECT id, name FROM MY_PLACES WHERE haversine_distance(lat1, lng1, lat2, lng2, 'ft') < 25;

以获取小于25英尺的所有记录。

核心功能是:

double
haversine_distance( UDF_INIT* initid, UDF_ARGS* args, char* is_null, char *error ) {
    double result = *(double*) initid->ptr;
    /*Earth Radius in Kilometers.*/ 
    double R = 6372.797560856;
    double DEG_TO_RAD = M_PI/180.0;
    double RAD_TO_DEG = 180.0/M_PI;
    double lat1 = *(double*) args->args[0];
    double lon1 = *(double*) args->args[1];
    double lat2 = *(double*) args->args[2];
    double lon2 = *(double*) args->args[3];
    double dlon = (lon2 - lon1) * DEG_TO_RAD;
    double dlat = (lat2 - lat1) * DEG_TO_RAD;
    double a = pow(sin(dlat * 0.5),2) + 
        cos(lat1*DEG_TO_RAD) * cos(lat2*DEG_TO_RAD) * pow(sin(dlon * 0.5),2);
    double c = 2.0 * atan2(sqrt(a), sqrt(1-a));
    result = ( R * c );
    /*
     * If we have a 5th distance type argument...
     */
    if (args->arg_count == 5) {
        str_to_lowercase(args->args[4]);
        if (strcmp(args->args[4], "ft") == 0) result *= 3280.8399;
        if (strcmp(args->args[4], "mi") == 0) result *= 0.621371192;
    }

    return result;
}

3

球形投影可以实现快速,简单和准确的近似(对于较小的距离)。与正确的计算相比,至少在我的路由算法中,我得到了20%的提升。在Java代码中,它看起来像:

public double approxDistKm(double fromLat, double fromLon, double toLat, double toLon) {
    double dLat = Math.toRadians(toLat - fromLat);
    double dLon = Math.toRadians(toLon - fromLon);
    double tmp = Math.cos(Math.toRadians((fromLat + toLat) / 2)) * dLon;
    double d = dLat * dLat + tmp * tmp;
    return R * Math.sqrt(d);
}

不确定MySQL(抱歉!)。

确保您了解限制(assertEquals的第三个参数表示以千米为单位的精度):

    float lat = 24.235f;
    float lon = 47.234f;
    CalcDistance dist = new CalcDistance();
    double res = 15.051;
    assertEquals(res, dist.calcDistKm(lat, lon, lat - 0.1, lon + 0.1), 1e-3);
    assertEquals(res, dist.approxDistKm(lat, lon, lat - 0.1, lon + 0.1), 1e-3);

    res = 150.748;
    assertEquals(res, dist.calcDistKm(lat, lon, lat - 1, lon + 1), 1e-3);
    assertEquals(res, dist.approxDistKm(lat, lon, lat - 1, lon + 1), 1e-2);

    res = 1527.919;
    assertEquals(res, dist.calcDistKm(lat, lon, lat - 10, lon + 10), 1e-3);
    assertEquals(res, dist.approxDistKm(lat, lon, lat - 10, lon + 10), 10);


3

阅读《用MySQL进行地理距离搜索》,这是一种基于Haversine Formula对MySQL的实现的解决方案。这是一个完整的解决方案描述,包括理论,实现和进一步的性能优化。尽管在我的情况下,空间优化部分无法正常工作。

我注意到了两个错误:

  1. abs在p8的select语句中的使用。我只是省略了abs,它起作用了。

  2. p27上的空间搜索距离函数不会转换为弧度或乘以经度cos(latitude),除非考虑到其空间数据已加载(无法从文章的上下文中得知),但是p26上的示例表明他的空间数据POINT未加载弧度弧度或度。


0
$objectQuery = "SELECT table_master.*, ((acos(sin((" . $latitude . "*pi()/180)) * sin((`latitude`*pi()/180))+cos((" . $latitude . "*pi()/180)) * cos((`latitude`*pi()/180)) * cos(((" . $longitude . "- `longtude`)* pi()/180))))*180/pi())*60*1.1515  as distance FROM `table_post_broadcasts` JOIN table_master ON table_post_broadcasts.master_id = table_master.id WHERE table_master.type_of_post ='type' HAVING distance <='" . $Radius . "' ORDER BY distance asc";

0

使用mysql

SET @orig_lon = 1.027125;
SET @dest_lon = 1.027125;

SET @orig_lat = 2.398441;
SET @dest_lat = 2.398441;

SET @kmormiles = 6371;-- for distance in miles set to : 3956

SELECT @kmormiles * ACOS(LEAST(COS(RADIANS(@orig_lat)) * 
 COS(RADIANS(@dest_lat)) * COS(RADIANS(@orig_lon - @dest_lon)) + 
 SIN(RADIANS(@orig_lat)) * SIN(RADIANS(@dest_lat)),1.0)) as distance;

参见:https : //andrew.hedges.name/experiments/haversine/

参见:https : //stackoverflow.com/a/24372831/5155484

参见:http : //www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

注意:LEAST用于避免空值,如https://stackoverflow.com/a/24372831/5155484上建议的注释

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.