MySQL大圆距离计算(Haversine公式)

193

我有一个工作正常的 PHP 脚本,可以获取经度和纬度值,然后将它们输入到 MySQL 查询中。我想将它转换为仅使用 MySQL。以下是我的当前 PHP 代码:

if ($distance != "Any" && $customer_zip != "") { //get the great circle distance

    //get the origin zip code info
    $zip_sql = "SELECT * FROM zip_code WHERE zip_code = '$customer_zip'";
    $result = mysql_query($zip_sql);
    $row = mysql_fetch_array($result);
    $origin_lat = $row['lat'];
    $origin_lon = $row['lon'];

    //get the range
    $lat_range = $distance/69.172;
    $lon_range = abs($distance/(cos($details[0]) * 69.172));
    $min_lat = number_format($origin_lat - $lat_range, "4", ".", "");
    $max_lat = number_format($origin_lat + $lat_range, "4", ".", "");
    $min_lon = number_format($origin_lon - $lon_range, "4", ".", "");
    $max_lon = number_format($origin_lon + $lon_range, "4", ".", "");
    $sql .= "lat BETWEEN '$min_lat' AND '$max_lat' AND lon BETWEEN '$min_lon' AND '$max_lon' AND ";
    }

有没有人知道如何完全使用MySQL完成此操作?我浏览了一下互联网,但大多数文献都相当令人困惑。


5
根据下面所有精彩的回答,这是Haversine公式实际应用的工作示例 - Michael M
https://dev59.com/c5vga4cB1Zd3GeqP8d5Q#40272394 这里有一个如何确保索引被命中的示例。 - exussum
9个回答

381

以下内容来自Google Code FAQ - 使用PHP、MySQL和Google Maps创建门店定位器:

这是一个SQL语句,它将查找距离37, -122坐标25英里范围内最接近的20个位置。它基于该行的纬度/经度和目标纬度/经度计算距离,然后只查询距离小于25的行,并按距离排序整个查询,限制结果为20个。要使用公里而不是英里搜索,请将3959替换为6371。

SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) 
* cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin(radians(lat)) ) ) AS distance 
FROM markers 
HAVING distance < 25 
ORDER BY distance 
LIMIT 0 , 20;

38
请将37和-122替换为你的坐标。 - Pavel Chuchuva
5
如果有数百万个地点(+成千上万的访问者),我会对此的性能影响感到好奇。 - Halil Özgür
13
您可以按照这份文档中的说明缩小查询范围以获得更好的性能: http://tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL - maliayas
2
@FosAvance 是的,如果您有一个带有id、lan和lng字段的markers表,那么这个查询就可以工作。 - Pavel Chuchuva
2
说到性能,有人能告诉我谷歌的版本是否比这个更好吗:((ACOS(SIN(51.5073509 * PI() / 180) * SIN(lat * PI() / 180) + COS(51.5073509 * PI() / 180) * COS(lat * PI() / 180) * COS((-0.1277583 - long) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) - Eugen Zaharia
显示剩余12条评论

36

$greatCircleDistance = acos( cos($latitude0) * cos($latitude1) * cos($longitude0 - $longitude1) + sin($latitude0) * sin($latitude1));

其中纬度和经度都是用弧度表示的。

因此:

SELECT 
  acos( 
      cos(radians( $latitude0 ))
    * cos(radians( $latitude1 ))
    * cos(radians( $longitude0 ) - radians( $longitude1 ))
    + sin(radians( $latitude0 )) 
    * sin(radians( $latitude1 ))
  ) AS greatCircleDistance 
 FROM yourTable;

这是您的SQL查询结果

要将结果以公里或英里表示,将结果乘以地球的平均半径(3959 英里, 6371 公里或 3440 海里)

在您的示例中计算的是边界框。如果将您的坐标数据放入启用空间数据的MySQL列中,则可以使用MySQL内置功能查询数据。

SELECT 
  id
FROM spatialEnabledTable
WHERE 
  MBRWithin(ogc_point, GeomFromText('Polygon((0 0,0 3,3 3,3 0,0 0))'))

14

如果您在坐标表中添加辅助字段,可以提高查询的响应时间。

示例:

CREATE TABLE `Coordinates` (
`id` INT(10) UNSIGNED NOT NULL COMMENT 'id for the object',
`type` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'type',
`sin_lat` FLOAT NOT NULL COMMENT 'sin(lat) in radians',
`cos_cos` FLOAT NOT NULL COMMENT 'cos(lat)*cos(lon) in radians',
`cos_sin` FLOAT NOT NULL COMMENT 'cos(lat)*sin(lon) in radians',
`lat` FLOAT NOT NULL COMMENT 'latitude in degrees',
`lon` FLOAT NOT NULL COMMENT 'longitude in degrees',
INDEX `lat_lon_idx` (`lat`, `lon`)
)    

如果你正在使用TokuDB,如果在任何谓词上添加聚集索引(例如像这样),那么性能将会更加出色:

alter table Coordinates add clustering index c_lat(lat);
alter table Coordinates add clustering index c_lon(lon);

对于每个点,您需要基本的经度和纬度以及弧度中的sin(lat),cos(lat)* cos(lon)和cos(lat)* sin(lon)。

然后创建一个mysql函数,类似于以下内容:

CREATE FUNCTION `geodistance`(`sin_lat1` FLOAT,
                              `cos_cos1` FLOAT, `cos_sin1` FLOAT,
                              `sin_lat2` FLOAT,
                              `cos_cos2` FLOAT, `cos_sin2` FLOAT)
    RETURNS float
    LANGUAGE SQL
    DETERMINISTIC
    CONTAINS SQL
    SQL SECURITY INVOKER
   BEGIN
   RETURN acos(sin_lat1*sin_lat2 + cos_cos1*cos_cos2 + cos_sin1*cos_sin2);
   END

这将给您距离。

不要忘记在lat/lon上添加索引,以便边界框可以帮助搜索而不会减慢速度(索引已在上面的CREATE TABLE查询中添加)。

INDEX `lat_lon_idx` (`lat`, `lon`)

如果有一个只有纬度/经度坐标的旧表格,您可以设置一个脚本来更新它,如下所示:(使用MeekroDB的PHP)

$users = DB::query('SELECT id,lat,lon FROM Old_Coordinates');

foreach ($users as $user)
{
  $lat_rad = deg2rad($user['lat']);
  $lon_rad = deg2rad($user['lon']);

  DB::replace('Coordinates', array(
    'object_id' => $user['id'],
    'object_type' => 0,
    'sin_lat' => sin($lat_rad),
    'cos_cos' => cos($lat_rad)*cos($lon_rad),
    'cos_sin' => cos($lat_rad)*sin($lon_rad),
    'lat' => $user['lat'],
    'lon' => $user['lon']
  ));
}

然后,您将优化实际查询,仅在确实需要时执行距离计算,例如通过从内部和外部限定圆形(或椭圆形)进行边界处理。 为此,您需要预先计算查询本身的几个度量。

// assuming the search center coordinates are $lat and $lon in degrees
// and radius in km is given in $distance
$lat_rad = deg2rad($lat);
$lon_rad = deg2rad($lon);
$R = 6371; // earth's radius, km
$distance_rad = $distance/$R;
$distance_rad_plus = $distance_rad * 1.06; // ovality error for outer bounding box
$dist_deg_lat = rad2deg($distance_rad_plus); //outer bounding box
$dist_deg_lon = rad2deg($distance_rad_plus/cos(deg2rad($lat)));
$dist_deg_lat_small = rad2deg($distance_rad/sqrt(2)); //inner bounding box
$dist_deg_lon_small = rad2deg($distance_rad/cos(deg2rad($lat))/sqrt(2));

假设已经做好了这些准备,查询大致如下(使用php):

$neighbors = DB::query("SELECT id, type, lat, lon,
       geodistance(sin_lat,cos_cos,cos_sin,%d,%d,%d) as distance
       FROM Coordinates WHERE
       lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d
       HAVING (lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d) OR distance <= %d",
  // center radian values: sin_lat, cos_cos, cos_sin
       sin($lat_rad),cos($lat_rad)*cos($lon_rad),cos($lat_rad)*sin($lon_rad),
  // min_lat, max_lat, min_lon, max_lon for the outside box
       $lat-$dist_deg_lat,$lat+$dist_deg_lat,
       $lon-$dist_deg_lon,$lon+$dist_deg_lon,
  // min_lat, max_lat, min_lon, max_lon for the inside box
       $lat-$dist_deg_lat_small,$lat+$dist_deg_lat_small,
       $lon-$dist_deg_lon_small,$lon+$dist_deg_lon_small,
  // distance in radians
       $distance_rad);

以上查询中的EXPLAIN可能会显示,除非有足够的结果触发索引,否则不会使用索引。只有在坐标表中有足够的数据时才会使用索引。 您可以在SELECT语句中添加FORCE INDEX(lat_lon_idx),以使其在不考虑表大小的情况下使用索引,因此您可以使用EXPLAIN验证其是否正确使用。

通过上述代码示例,您应该能够实现一个可扩展的、具有最小误差的距离对象搜索。


11

我必须详细地研究这个问题,所以我将分享我的结果。这使用一个带有纬度经度表的zip表。它不依赖于Google Maps;相反,您可以将其适应于包含纬度/经度的任何表格。

SELECT zip, primary_city, 
       latitude, longitude, distance_in_mi
  FROM (
SELECT zip, primary_city, latitude, longitude,r,
       (3963.17 * ACOS(COS(RADIANS(latpoint)) 
                 * COS(RADIANS(latitude)) 
                 * COS(RADIANS(longpoint) - RADIANS(longitude)) 
                 + SIN(RADIANS(latpoint)) 
                 * SIN(RADIANS(latitude)))) AS distance_in_mi
 FROM zip
 JOIN (
        SELECT  42.81  AS latpoint,  -70.81 AS longpoint, 50.0 AS r
   ) AS p 
 WHERE latitude  
  BETWEEN latpoint  - (r / 69) 
      AND latpoint  + (r / 69)
   AND longitude 
  BETWEEN longpoint - (r / (69 * COS(RADIANS(latpoint))))
      AND longpoint + (r / (69 * COS(RADIANS(latpoint))))
  ) d
 WHERE distance_in_mi <= r
 ORDER BY distance_in_mi
 LIMIT 30

看一下那个查询语句中间的这一行:

    SELECT  42.81  AS latpoint,  -70.81 AS longpoint, 50.0 AS r

这个查询在距离纬度42.81/-70.81的点50英里范围内,寻找zip表中最近的30个条目。当您将其构建为应用程序时,这就是您放置自己的点和搜索半径的地方。

如果你想使用公里而不是英里工作,在查询中将69更改为111.045,将3963.17更改为6378.10

这里有一个详细的写作。希望对某人有所帮助。http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/


4
 SELECT *, (  
    6371 * acos(cos(radians(search_lat)) * cos(radians(lat) ) *   
cos(radians(lng) - radians(search_lng)) + sin(radians(search_lat)) *         sin(radians(lat)))  
) AS distance  
FROM table  
WHERE lat != search_lat AND lng != search_lng AND distance < 25  
 ORDER BY distance  
FETCH 10 ONLY 

距离为25公里


最后一个(radians(lat))必须是sin(radians(lat))。 - KGs
我收到一个错误信息:“未知的列距离”,这是为什么? - user12173484
@JillJohn 如果你只需要距离,那么可以完全删除按距离排序的部分。如果你想要对结果进行排序,可以使用以下代码 - ORDER BY (
6371 * acos(cos(radians(search_lat)) * cos(radians(lat) ) *
cos(radians(lng) - radians(search_lng)) + sin(radians(search_lat)) * sin(radians(lat)))
).
- Harish Lalwani

3
我已经编写了一个程序,可以计算相同的内容,但是您需要在对应的表格中输入纬度和经度。
drop procedure if exists select_lattitude_longitude;

delimiter //

create procedure select_lattitude_longitude(In CityName1 varchar(20) , In CityName2 varchar(20))

begin

    declare origin_lat float(10,2);
    declare origin_long float(10,2);

    declare dest_lat float(10,2);
    declare dest_long float(10,2);

    if CityName1  Not In (select Name from City_lat_lon) OR CityName2  Not In (select Name from City_lat_lon) then 

        select 'The Name Not Exist or Not Valid Please Check the Names given by you' as Message;

    else

        select lattitude into  origin_lat from City_lat_lon where Name=CityName1;

        select longitude into  origin_long  from City_lat_lon where Name=CityName1;

        select lattitude into  dest_lat from City_lat_lon where Name=CityName2;

        select longitude into  dest_long  from City_lat_lon where Name=CityName2;

        select origin_lat as CityName1_lattitude,
               origin_long as CityName1_longitude,
               dest_lat as CityName2_lattitude,
               dest_long as CityName2_longitude;

        SELECT 3956 * 2 * ASIN(SQRT( POWER(SIN((origin_lat - dest_lat) * pi()/180 / 2), 2) + COS(origin_lat * pi()/180) * COS(dest_lat * pi()/180) * POWER(SIN((origin_long-dest_long) * pi()/180 / 2), 2) )) * 1.609344 as Distance_In_Kms ;

    end if;

end ;

//

delimiter ;

3

我无法对上面的答案进行评论,但是要小心@Pavel Chuchuva的答案。如果两个坐标相同,那么这个公式不会返回结果。在这种情况下,距离为null,因此该行不会像当前公式那样返回。

虽然我不是MySQL专家,但是这个公式对我来说似乎有效:

SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance 
FROM markers HAVING distance < 25 OR distance IS NULL ORDER BY distance LIMIT 0 , 20;

2
如果位置相同,它不应该输出NULL,而应该输出零(因为ACOS(1)为0)。您可能会看到x轴* x轴+ y轴* y轴+ z轴* z轴超出ACOS范围的四舍五入问题,但您似乎没有防范这种情况? - Rowland Shaw

2

我认为我的JavaScript实现可以作为一个不错的参考:

/*
 * Check to see if the second coord is within the precision ( meters )
 * of the first coord and return accordingly
 */
function checkWithinBound(coord_one, coord_two, precision) {
    var distance = 3959000 * Math.acos( 
        Math.cos( degree_to_radian( coord_two.lat ) ) * 
        Math.cos( degree_to_radian( coord_one.lat ) ) * 
        Math.cos( 
            degree_to_radian( coord_one.lng ) - degree_to_radian( coord_two.lng ) 
        ) +
        Math.sin( degree_to_radian( coord_two.lat ) ) * 
        Math.sin( degree_to_radian( coord_one.lat ) ) 
    );
    return distance <= precision;
}

/**
 * Get radian from given degree
 */
function degree_to_radian(degree) {
    return degree * (Math.PI / 180);
}

2

calculate distance in Mysql

 SELECT (6371 * acos(cos(radians(lat2)) * cos(radians(lat1) ) * cos(radians(long1) -radians(long2)) + sin(radians(lat2)) * sin(radians(lat1)))) AS distance

因此,距离值将被计算,任何人都可以根据需要应用。最初的回答。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接