Khoảng cách vòng tròn lớn của MySQL (công thức Haversine)


168

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 một truy vấn MySQL. Tôi muốn biến nó thành MySQL duy nhất. Đâ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 cách làm cho MySQL hoàn toàn không? Tôi đã duyệt Internet một chút nhưng hầu hết các tài liệu trên nó là khá khó hiểu.

+4

Dựa trên tất cả các câu trả lời tuyệt vời dưới đây, [ở đây đang làm việc mẫu của công thức Haversine trong hành động] (http://sqlfiddle.com/#!2/abba1/2/0) 19 jan. 142014-01-19 06:29:13

  0

Cảm ơn bạn đã chia sẻ Michael.M 19 jan. 142014-01-19 11:03:31

  0

http://stackoverflow.com/a/40272394/1281385 Có một ví dụ về cách đảm bảo chỉ mục được nhấn 26 oct. 162016-10-26 21:32:09

311

Từ Google Code FAQ - Creating a Store Locator with PHP, MySQL & 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 độ mục tiêu, sau đó chỉ yêu cầu các hàng có giá trị khoảng cách nhỏ hơn 25, đặt hàng toàn bộ truy vấn theo khoảng cách và giới hạn nó thành 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; 
  0

Chính xác những gì tôi cần, cảm ơn. 11 may. 112011-05-11 18:56:07

+2

câu lệnh sql thực sự tốt. nhưng tôi có thể chuyển tọa độ của tôi vào câu lệnh này ở đâu? tôi không thể thấy bất kỳ tọa độ nào đã qua 13 aug. 112011-08-13 14:43:40

+28

Thay thế 37 và -122 bằng tọa độ của bạn. 14 aug. 112011-08-14 05:32:53

+4

Tôi tự hỏi về tác động của điều này nếu có hàng triệu địa điểm (+ hàng nghìn khách truy cập) ... 01 dec. 112011-12-01 10:26:08

  0

bộ nhớ đệm luôn là bạn của bạn trong trường hợp đó 21 feb. 122012-02-21 22:13:58

+11

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: http : //tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL 15 nov. 122012-11-15 01:25:40

  0

Bài viết tuyệt vời, cảm ơn @maliayas 11 jul. 132013-07-11 16:40:17

  0

-1 vì không thay thế các hằng số với các biến lat & lon 12 mar. 142014-03-12 08:27:13

  0

là "lat" và "lng "các trường trong cơ sở dữ liệu? 29 mar. 142014-03-29 16:49:03

+2

@FosAvance Có, truy vấn này sẽ hoạt động nếu bạn có bảng 'markers' với các trường id, lan và lng. 29 mar. 142014-03-29 23:08:31

  0

nói về hiệu suất, ai đó có thể cho tôi biết nếu phiên bản của Google tốt hơn phiên bản này: __ ((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) __ 20 mar. 152015-03-20 09:20:59

+1

@EugenZaharia Công thức mà bạn đang đề cập đến là định luật hình cầu của cosin và gần giống với công thức Haversine. Định luật hình cầu của công thức cosin trả về kết quả chính xác giống như công thức Haversine cho khoảng cách lớn hơn vài mét, và nhanh gấp hai lần. 10 apr. 152015-04-10 14:56:25

  0

Tôi muốn thêm truy vấn LIKE sql trong này.? 27 oct. 152015-10-27 09:00:37

  0

Công thức này là lỗi. Tôi đã sử dụng một cái rất giống nhau (tính bằng kilômét) và nếu một điểm chính xác ở giữa vòng tròn (bán kính) thì điểm này sẽ không được chọn. Tôi đang đấu tranh để giải quyết lỗi này, kiểm tra điều này -> http://stackoverflow.com/questions/42522005/select-points-from-map-database-according-to-radius 01 mar. 172017-03-01 03:34:31

  0

Bây giờ có lỗi trên trang google gốc. Đoạn mã vẫn còn 37 và -122 như lat/lng trong truy vấn, nhưng các văn bản hiện nay nói "Đây là câu lệnh SQL mà tìm thấy 20 địa điểm gần nhất trong vòng bán kính 25 dặm về phía -33, 151 phối hợp" - điều này làm nó khá khó hiểu! 07 jun. 172017-06-07 12:48:06


28

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

với vĩ độ và kinh độ trong radian.

nên

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

là bạn truy vấn SQL

để 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 (3959 dặm, 6371 Km hoặc 3440 nautical miles)

Điều bạn đang tính toán trong ví dụ của mình là một hộp giới hạn. Nếu bạn đặt dữ liệu tọa độ của mình theo số spatial enabled MySQL column, bạn có thể sử dụng MySQL's build in functionality để 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))')) 

3

Tôi đã viết một quy trình có thể tính toán tương tự, nhưng bạn phải nhập vĩ độ và kinh độ trong 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 ; 

13

Nếu bạn thêm trường 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ó được hiệu suất tốt hơn nếu bạn thêm phân nhóm chỉ số 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) theo radian, cos (lat) * cos (lon) theo radian và cos (lat) * sin (lon) theo 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 

này cung cấp cho bạn khoảng cách.

Đừng quên thêm chỉ mục vào lat/lon để boxing giới hạn có thể giúp tìm kiếm thay vì làm chậm nó xuống (chỉ mục đã được thêm vào truy vấn TẠO TẠO ở trên).

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

Với một bảng cũ chỉ với tọa độ/lon lat, bạn có thể thiết lập một kịch bản để 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 phép tính khoảng cách khi thực sự cần thiết, ví dụ bằng cách bao quanh hình tròn (tốt, hình bầu dục) từ trong và ngoài. Cho rằng, bạn sẽ cần phải precalculate vài số liệu cho truy vấn bản thâ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 chuẩn bị, truy vấn đi một cái gì đó 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 trên các 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 lệnh SELECT để sử dụng chỉ mục không liên quan đến kích thước bảng, vì vậy bạn có thể xác minh bằng GIẢI THÍCH rằng nó hoạt động chính xác.

Với các mẫu mã ở trên, bạn nên thực hiện tìm kiếm đối tượng đang hoạt động và có thể mở rộng theo khoảng cách với lỗi tối thiểu.


2

Tôi nghĩ thực hiện javascript của tôi sẽ là một tài liệu tham khảo tốt để:

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

3

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

Tôi không phải là một chuyên gia về MySQL, nhưng điều này dường như được làm việc cho 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ó * không nên * đi ra NULL, nhưng bằng không (như 'ACOS (1)' là 0). Bạn * có thể * thấy các vấn đề làm tròn với xaxis * xaxis + yaxis * yaxis + zaxis * zaxis đi ra khỏi phạm vi cho ACOS nhưng bạn không xuất hiện để được bảo vệ chống lại điều đó? 15 mar. 132013-03-15 13:18:47


8

Tôi đã phải làm việc này ra một cách chi tiết, vì vậy tôi sẽ chia sẻ kết quả của tôi. Điều này sử dụng bảng zip với các bảng latitudelongitude. Nó không phụ thuộc vào Google Maps; thay vào đó bạn có thể thích nghi nó với bất kỳ bảng nào có 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 rằng:

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

này tìm kiếm cho 30 mục gần nhất trong bảng zip 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 bán kính tìm kiếm và điểm của riêng mình.

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

Dưới đây là phần ghi chi tiết. Tôi hy vọng nó giúp ai đó.http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/


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 

ở cự ly 25 km

  0

Lần cuối cùng (radian (lat) phải là sin (radian (lat)) 08 mar. 172017-03-08 20:01:25

  0

@KGs cảm ơn. Đã chỉnh sửa theo đề xuất của bạn. 08 mar. 182018-03-08 14:33:10