MySQL Great Circle Distance (rumus Haversine)

184

Saya punya skrip PHP yang berfungsi yang mendapatkan nilai Bujur dan Latitude lalu memasukkannya ke dalam kueri MySQL. Saya ingin membuatnya hanya MySQL. Inilah kode PHP saya saat ini:

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

Adakah yang tahu cara membuat ini sepenuhnya MySQL? Saya sudah menjelajah internet sedikit, tetapi sebagian besar literatur tentangnya cukup membingungkan.

Nick Woodham
sumber
4
Berdasarkan semua jawaban yang sangat baik di bawah ini, berikut adalah contoh kerja dari formula Haversine dalam aksi
StartupGuy
Terima kasih telah berbagi tentang Michael.M
Nick Woodhams
stackoverflow.com/a/40272394/1281385 Memiliki contoh bagaimana memastikan indeks terkena
exussum

Jawaban:

357

Dari Google Code FAQ - Membuat Pencari Lokasi Toko dengan PHP, MySQL & Google Maps :

Inilah pernyataan SQL yang akan menemukan 20 lokasi terdekat yang berada dalam radius 25 mil ke 37, -122 koordinat. Ini menghitung jarak berdasarkan garis lintang / bujur dari baris itu dan garis lintang / bujur target, dan kemudian meminta hanya baris di mana nilai jarak kurang dari 25, memesan seluruh kueri berdasarkan jarak, dan membatasi ke 20 hasil. Untuk mencari menurut kilometer bukannya mil, ganti 3959 dengan 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;
Pavel Chuchuva
sumber
2
pernyataan sql sangat bagus. tetapi di mana saya bisa mengirimkan koordinat saya ke dalam pernyataan ini? saya tidak bisa melihat di mana saja koordinat telah lewat
Mann
32
Ganti 37 dan -122 dengan koordinat Anda.
Pavel Chuchuva
5
Saya bertanya-tanya tentang implikasi kinerja ini jika ada jutaan tempat (+ ribuan pengunjung) ...
Halil Özgür
12
Anda dapat mempersempit kueri untuk kinerja yang lebih baik seperti yang dijelaskan dalam dokumen ini: tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
maliayas
2
@FosAvance Ya, kueri ini akan berfungsi jika Anda memiliki markerstabel dengan bidang id, lan dan lng.
Pavel Chuchuva
32

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

dengan lintang dan bujur di radian.

begitu

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

adalah permintaan SQL Anda

untuk mendapatkan hasil Anda dalam Km atau mil, kalikan hasilnya dengan radius rata-rata Bumi ( 3959mil, 6371Km atau 3440mil laut)

Hal yang Anda hitung dalam contoh Anda adalah kotak pembatas. Jika Anda memasukkan data koordinat Anda dalam kolom MySQL yang diaktifkan secara spasial , Anda dapat menggunakan fungsionalitas build in MySQL untuk meminta data.

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

Jika Anda menambahkan bidang pembantu ke tabel koordinat, Anda dapat meningkatkan waktu respons kueri.

Seperti ini:

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

Jika Anda menggunakan TokuDB, Anda akan mendapatkan kinerja yang lebih baik jika Anda menambahkan indeks pengelompokan pada salah satu predikat, misalnya, seperti ini:

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

Anda membutuhkan lat dasar dan lon dalam derajat dan juga dosa (lat) dalam radian, cos (lat) * cos (lon) dalam radian dan cos (lat) * sin (lon) dalam radian untuk setiap titik. Kemudian Anda membuat fungsi mysql, seperti ini:

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

Ini memberi Anda jarak.

Jangan lupa untuk menambahkan indeks pada lat / lon sehingga tinju pembatas dapat membantu pencarian alih-alih memperlambatnya (indeks sudah ditambahkan dalam kueri CREATE TABLE di atas).

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

Diberikan tabel lama dengan hanya koordinat lat / lon, Anda dapat mengatur skrip untuk memperbaruinya seperti ini: (php menggunakan 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']
  ));
}

Kemudian Anda mengoptimalkan kueri yang sebenarnya untuk hanya melakukan perhitungan jarak ketika benar-benar diperlukan, misalnya dengan membatasi lingkaran (baik, oval) dari dalam dan luar. Untuk itu, Anda harus menghitung ulang beberapa metrik untuk kueri itu sendiri:

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

Mengingat persiapan itu, kueri berlangsung seperti ini (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);

JELASKAN pada kueri di atas mungkin mengatakan bahwa itu tidak menggunakan indeks kecuali ada cukup hasil untuk memicu seperti itu. Indeks akan digunakan ketika ada cukup data dalam tabel koordinat. Anda dapat menambahkan FORCE INDEX (lat_lon_idx) ke SELECT untuk membuatnya menggunakan indeks tanpa memperhatikan ukuran tabel, sehingga Anda dapat memverifikasi dengan EXPLAIN bahwa itu berfungsi dengan benar.

Dengan contoh kode di atas Anda harus memiliki implementasi pencarian objek yang berjalan dan dapat diukur berdasarkan jarak dengan kesalahan minimal.

silvio
sumber
10

Saya harus menyelesaikan ini secara terperinci, jadi saya akan membagikan hasilnya. Ini menggunakan ziptabel dengan latitudedan longitudetabel. Itu tidak tergantung pada Google Maps; Anda bisa mengadaptasinya ke tabel mana saja yang berisi 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

Lihat baris ini di tengah permintaan itu:

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

Ini mencari 30 entri terdekat di zip tabel dalam jarak 50.0 mil dari titik lat / long 42.81 / -70.81. Ketika Anda membuat ini menjadi sebuah aplikasi, di situlah Anda meletakkan titik Anda sendiri dan radius pencarian.

Jika Anda ingin bekerja dalam kilometer daripada mil, ganti 69ke 111.045dan ubah 3963.17ke6378.10 dalam kueri.

Ini langganan lengkap. Saya harap ini membantu seseorang. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

O. Jones
sumber
3

Saya telah menulis prosedur yang dapat menghitung hal yang sama, tetapi Anda harus memasukkan garis lintang dan bujur di tabel masing-masing.

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 ;
Abdul Manaf
sumber
3

Saya tidak dapat mengomentari jawaban di atas, tetapi berhati-hatilah dengan jawaban @Pavel Chuchuva. Rumus itu tidak akan mengembalikan hasil jika kedua koordinat sama. Dalam hal ini, jarak adalah nol, dan sehingga baris itu tidak akan dikembalikan dengan rumus itu apa adanya.

Saya bukan ahli MySQL, tetapi ini sepertinya bekerja untuk saya:

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;
John Crenshaw
sumber
2
Jika posisi identik, tidak boleh keluar NULL, tetapi nol (seperti ACOS(1)0). Anda mungkin melihat masalah pembulatan dengan xaxis * xaxis + yaxis * yaxis + zaxis * zaxis keluar dari jangkauan untuk ACOS tetapi Anda tampaknya tidak menjaga hal itu?
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 

untuk jarak 25 km

Harish Lalwani
sumber
Yang terakhir (radian (lat) haruslah dosa (radian (lat))
KG
saya mendapatkan kesalahan "jarak kolom tidak dikenal" mengapa ini?
Jill John
@JillJohn jika Anda hanya ingin jarak maka Anda dapat menghapus pesanan dengan jarak sepenuhnya. Jika Anda ingin mengurutkan hasil, Anda dapat menggunakan ini - 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

Saya pikir implementasi javascript saya akan menjadi referensi yang bagus untuk:

/*
 * 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);
}
Sam Vloeberghs
sumber
0

menghitung jarak dalam Mysql

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

dengan demikian nilai jarak akan dihitung dan siapa pun dapat menerapkan seperti yang disyaratkan.

Rajesh Prasad Yadav
sumber