MySQL Great Circle Khoảng cách (công thức Haversine)


184

Tôi đã có một tập lệnh PHP đang hoạt động nhận các giá trị Kinh độ và Vĩ độ và sau đó nhập chúng vào truy vấn MySQL. Tôi muốn làm cho nó chỉ có MySQL. Đây là Mã PHP hiện tại của tôi:

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 ";
    }

Có ai biết làm thế nào để làm điều này hoàn toàn MySQL? Tôi đã duyệt Internet một chút nhưng hầu hết các tài liệu về nó đều khá khó hiểu.


4
Dựa trên tất cả các câu trả lời xuất sắc dưới đây, đây là mẫu hoạt động của công thức Haversine đang hoạt động
StartupGuy

Cảm ơn đã chia sẻ rằng Michael.M
Nick Woodhams

stackoverflow.com/a/40272394/1281385 Có một ví dụ về cách đảm bảo chỉ số được nhấn
exussum

Câu trả lời:


357

Từ Câu hỏi thường gặp về Google Code - Tạo Trình định vị cửa hàng bằng PHP, MySQL và Google Maps :

Dưới đây là các câu lệnh SQL mà sẽ tìm ra 20 địa điểm gần nhất mà là trong vòng bán kính 25 dặm về phía 37, -122 phối hợp. Nó tính toán khoảng cách dựa trên vĩ độ / kinh độ của hàng đó và vĩ độ / kinh độ đích, sau đó chỉ yêu cầu các hàng có giá trị khoảng cách nhỏ hơn 25, sắp xếp toàn bộ truy vấn theo khoảng cách và giới hạn ở 20 kết quả. Để tìm kiếm theo km thay vì dặm, thay thế 3959 với 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;

2
tuyên bố sql là thực sự tốt. nhưng tôi có thể chuyển tọa độ của mình vào câu lệnh này ở đâu? tôi không thể thấy bất cứ nơi nào tọa độ đã đi qua
Mann

32
Thay thế 37 và -122 bằng tọa độ của bạn.
Pavel Chuchuva

5
Tôi tự hỏi về ý nghĩa hiệu suất của việc này nếu có hàng triệu địa điểm (+ hàng nghìn khách truy cập) ...
Halil zgür

12
Bạn có thể thu hẹp truy vấn để có hiệu suất tốt hơn như được giải thích trong tài liệu này: tr.scribed.com/doc/2569355/Geo-Distance-Search-with-MyQuery
maliayas

2
@FosAvance Có, truy vấn này sẽ hoạt động nếu bạn có markersbảng với các trường id, lan và lng.
Pavel Chuchuva

32

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

với vĩ độ và kinh độ tính theo radian.

vì thế

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

là truy vấn SQL của bạn

để có được kết quả của bạn trong Km hoặc dặm, nhân kết quả với bán kính trung bình của Trái đất ( 3959dặm, 6371Km hoặc 3440hải lý)

Thứ bạn đang tính toán trong ví dụ của bạn là một hộp giới hạn. Nếu bạn đặt dữ liệu tọa độ của mình vào cột MySQL được kích hoạt không gian , bạn có thể sử dụng chức năng xây dựng của MySQL để truy vấn dữ liệu.

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

13

Nếu bạn thêm các trường của trình trợ giúp vào bảng tọa độ, bạn có thể cải thiện thời gian phản hồi của truy vấn.

Như thế này:

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`)
)    

Nếu bạn đang sử dụng TokuDB, bạn sẽ có hiệu suất cao hơn nữa nếu bạn thêm các chỉ mục phân cụm trên một trong các vị từ, ví dụ như thế này:

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

Bạn sẽ cần lat và lon cơ bản về độ cũng như sin (lat) tính bằng radian, cos (lat) * cos (lon) tính bằng radian và cos (lat) * sin (lon) tính bằng radian cho mỗi điểm. Sau đó, bạn tạo một hàm mysql, smth như thế này:

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

Điều này cho bạn khoảng cách.

Đừng quên thêm chỉ mục vào lat / lon để quyền anh có thể giúp tìm kiếm thay vì làm chậm nó (chỉ mục đã được thêm vào trong truy vấn CREATE TABLE ở trên).

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

Đưa ra một bảng cũ chỉ có tọa độ lat / lon, bạn có thể thiết lập một tập lệnh để cập nhật nó như thế này: (php sử dụng meekrodb)

$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']
  ));
}

Sau đó, bạn tối ưu hóa truy vấn thực tế để chỉ thực hiện tính toán khoảng cách khi thực sự cần thiết, ví dụ bằng cách giới hạn vòng tròn (tốt, hình bầu dục) từ bên trong và bên ngoài. Vì thế, bạn sẽ cần tính toán trước một số số liệu cho chính truy vấn:

// 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));

Với những sự chuẩn bị đó, truy vấn sẽ giống như thế này (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);

GIẢI THÍCH cho truy vấn trên có thể nói rằng nó không sử dụng chỉ mục trừ khi có đủ kết quả để kích hoạt như vậy. Chỉ mục sẽ được sử dụng khi có đủ dữ liệu trong bảng tọa độ. Bạn có thể thêm FORCE INDEX (lat_lon_idx) vào CHỌN để làm cho nó sử dụng chỉ mục không liên quan đến kích thước bảng, do đó bạn có thể xác minh với EXPLAIN rằng nó đang hoạt động chính xác.

Với các mẫu mã ở trên, bạn sẽ có một triển khai tìm kiếm đối tượng hoạt động và có thể mở rộng theo khoảng cách với sai số tối thiểu.


10

Tôi đã phải làm việc này một cách chi tiết, vì vậy tôi sẽ chia sẻ kết quả của mình. Điều này sử dụng một zipbảng với latitudelongitudebảng. Nó không phụ thuộc vào Google Maps; thay vào đó bạn có thể điều chỉnh nó cho bất kỳ bảng nào chứa lat / long.

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

Nhìn vào dòng này ở giữa truy vấn đó:

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

Đây tìm kiếm cho 30 mục gần nhất trong zipbảng trong vòng 50.0 dặm của / điểm dài lat 42,81 / -70,81. Khi bạn xây dựng ứng dụng này thành một ứng dụng, đó là nơi bạn đặt điểm và bán kính tìm kiếm của riêng bạn.

Nếu bạn muốn làm việc ở cây số chứ không phải dặm, thay đổi 69để 111.045và thay đổi 3963.17để 6378.10trong truy vấn.

Đây là một bài viết chi tiết. Tôi hy vọng nó sẽ giúp được ai đó. http://www.plumislandmedia.net/mysql/haversine-mysql-gầnest-loc/


3

Tôi đã viết một thủ tục có thể tính toán tương tự, nhưng bạn phải nhập vĩ độ và kinh độ vào bảng tương ứng.

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

Tôi không thể nhận xét về câu trả lời trên, nhưng hãy cẩn thận với câu trả lời của @Pavel Chuchuva. Công thức đó sẽ không trả về kết quả nếu cả hai tọa độ giống nhau. Trong trường hợp đó, khoảng cách là null và vì vậy hàng đó sẽ không được trả về với công thức đó.

Tôi không phải là chuyên gia về MySQL, nhưng điều này dường như đang làm việc với tôi:

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
Nếu các vị trí giống hệt nhau, nó sẽ không xuất hiện NULL, nhưng là 0 (bằng ACOS(1)0). Bạn có thể thấy các vấn đề làm tròn với xaxis * xaxis + yaxis * yaxis + zaxis * zaxis sẽ vượt ra khỏi phạm vi của ACOS nhưng bạn dường như không đề phòng điều đó?
Rowland Shaw

3
 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 

cho khoảng cách 25 km


Cái cuối cùng (radian (lat) phải là sin (radian (lat))
KG

tôi nhận được một lỗi "khoảng cách cột không xác định" tại sao điều này?
Jill John

@JillJohn nếu bạn chỉ muốn khoảng cách thì bạn có thể xóa thứ tự theo khoảng cách hoàn toàn. Nếu bạn muốn sắp xếp kết quả, bạn có thể sử dụng kết quả này - ORDER BY (6371 * acos (cos (radian (search_lat)) * cos (radian (lat)) * cos (radian (lng) - radian (search_lng)) + sin (radian (search_lat)) * sin (radian (lat)))).
Harish Lalwani

2

Tôi nghĩ rằng việc triển khai javascript của tôi sẽ là một tài liệu tham khảo tốt về:

/*
 * 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);
}

0

tính khoảng cách trong Mysql

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

do đó, giá trị khoảng cách sẽ được tính toán và bất cứ ai cũng có thể áp dụng theo yêu cầu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.