MySQL Great Circle Distance (formuła Haversine)

184

Mam działający skrypt PHP, który pobiera wartości długości i szerokości geograficznej, a następnie wprowadza je do zapytania MySQL. Chciałbym, aby był to wyłącznie MySQL. Oto mój obecny kod 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 ";
    }

Czy ktoś wie, jak zrobić to całkowicie MySQL? Przeglądałem trochę Internet, ale większość literatury na ten temat jest dość myląca.

Nick Woodhams
źródło
4
W oparciu o wszystkie doskonałe odpowiedzi poniżej, oto działająca próbka formuły Haversine w akcji
StartupGuy
Dzięki za udostępnienie tego Michael.M
Nick Woodhams
stackoverflow.com/a/40272394/1281385 Ma przykład, jak zapewnić trafienie indeksu
exussum

Odpowiedzi:

357

Z Google Code FAQ - Tworzenie lokalizatora sklepów za pomocą PHP, MySQL i Google Maps :

Oto instrukcja SQL, która znajdzie najbliższe 20 lokalizacji, które znajdują się w promieniu 25 mil od współrzędnej 37, -122. Oblicza odległość na podstawie szerokości / długości geograficznej tego wiersza i docelowej szerokości / długości geograficznej, a następnie prosi tylko o wiersze, w których wartość odległości jest mniejsza niż 25, porządkuje całe zapytanie według odległości i ogranicza go do 20 wyników. Aby wyszukiwać według kilometrów zamiast mil, zamień 3959 na 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
źródło
2
instrukcja sql jest naprawdę dobra. ale gdzie mogę przekazać moje współrzędne do tego oświadczenia? Nie widzę, gdziekolwiek przeszły współrzędne
Mann,
32
Zamień 37 i -122 na swoje współrzędne.
Pavel Chuchuva,
5
Zastanawiam się, jakie są tego skutki dla wydajności, jeśli istnieją miliony miejsc (+ tysiące odwiedzających) ...
Halil Özgür
12
Można zawęzić zapytanie w celu uzyskania lepszej wydajności, jak wyjaśniono w tym dokumencie: tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
maliayas
2
@FosAvance Tak, to zapytanie działałoby, jeśli masz markerstabelę z polami id, lan i lng.
Pavel Chuchuva
32

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

o szerokości i długości geograficznej w radianach.

więc

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

jest twoje zapytanie SQL

aby uzyskać wyniki w kilometrach lub milach, należy pomnożyć wynik przez średni promień Ziemi ( 3959mile, kilometry 6371lub 3440mile morskie)

To, co obliczasz w swoim przykładzie, to obwiednia. Jeśli umieścisz swoje dane współrzędnych w przestrzennym włączona kolumny MySQL , można użyć kompilacji MySQL funkcjonalności do kwerendy danych.

SELECT 
  id
FROM spatialEnabledTable
WHERE 
  MBRWithin(ogc_point, GeomFromText('Polygon((0 0,0 3,3 3,3 0,0 0))'))
Jacco
źródło
13

Jeśli dodasz pola pomocnicze do tabeli współrzędnych, możesz poprawić czas odpowiedzi na zapytanie.

Lubię to:

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

Jeśli używasz TokuDB, uzyskasz jeszcze lepszą wydajność, jeśli dodasz indeksy klastrowe w jednym z predykatów, na przykład w następujący sposób:

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

Będziesz potrzebował podstawowych lat i lon w stopniach, a także sin (lat) w radianach, cos (lat) * cos (lon) w radianach i cos (lat) * sin (lon) w radianach dla każdego punktu. Następnie tworzysz funkcję mysql, coś takiego:

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

To daje odległość.

Nie zapomnij dodać indeksu do lat / lon, aby obwiednia mogła pomóc w wyszukiwaniu zamiast spowalniać (indeks jest już dodany w zapytaniu CREATE TABLE powyżej).

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

Biorąc pod uwagę starą tabelę z tylko współrzędnymi lat / lon, możesz skonfigurować skrypt, aby aktualizować go w następujący sposób: (php za pomocą 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']
  ));
}

Następnie optymalizujesz rzeczywiste zapytanie, aby wykonać obliczenie odległości tylko wtedy, gdy jest to naprawdę potrzebne, na przykład ograniczając okrąg (dobrze, owalny) od wewnątrz i na zewnątrz. W tym celu musisz wstępnie obliczyć kilka metryk dla samego zapytania:

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

Biorąc pod uwagę te przygotowania, zapytanie przebiega mniej więcej tak (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);

WYJAŚNIJ na powyższe zapytanie może powiedzieć, że nie używa indeksu, chyba że istnieje wystarczająca liczba wyników, aby je wywołać. Indeks zostanie wykorzystany, gdy w tabeli współrzędnych będzie wystarczająca ilość danych. Możesz dodać FORCE INDEX (lat_lon_idx) do SELECT, aby używał indeksu bez względu na rozmiar tabeli, dzięki czemu możesz sprawdzić przy pomocy EXPLAIN, że działa poprawnie.

Z powyższymi przykładami kodu powinieneś mieć działającą i skalowalną implementację wyszukiwania obiektów według odległości z minimalnym błędem.

silvio
źródło
10

Musiałem to szczegółowo opracować, więc podzielę się swoim wynikiem. Ten wykorzystuje zipstół latitudei longitudestoły. To nie zależy od Google Maps; raczej możesz dostosować go do dowolnej tabeli zawierającej długość / szerokość.

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

Spójrz na ten wiersz w środku tego zapytania:

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

Wyszukuje 30 najbliższych pozycji w zip tabeli w odległości 50,0 mil od punktu szerokości / długości 42,81 / -70,81. Gdy wbudujesz to w aplikację, umieścisz własny punkt i promień wyszukiwania.

Jeśli chcesz pracować w kilometrach, a nie zmiana mil, 69aby 111.045i zmiany 3963.17do6378.10 w zapytaniu.

Oto szczegółowy opis. Mam nadzieję, że to komuś pomoże. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

O. Jones
źródło
3

Napisałem procedurę, która może obliczyć to samo, ale musisz wprowadzić szerokość i długość geograficzną w odpowiedniej tabeli.

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
źródło
3

Nie mogę skomentować powyższej odpowiedzi, ale bądź ostrożny z odpowiedzią @Pavel Chuchuva. Ta formuła nie zwróci wyniku, jeśli obie współrzędne są takie same. W takim przypadku odległość wynosi zero, więc wiersz nie zostanie zwrócony z tą formułą w takiej postaci, w jakiej jest.

Nie jestem ekspertem od MySQL, ale wydaje mi się, że to działa dla mnie:

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
źródło
2
Jeśli pozycje są identyczne, nie powinno wynosić NULL, ale jako zero (jak ACOS(1)0). Państwo może zobaczyć zaokrąglania problemy z Xaxis * Xaxis + yAxis * yAxis + zaxis * zaxis dzieje się poza zasięgiem dla ACO, ale nie wydaje się być ochronę przed tym?
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 

na odległość 25 km

Harish Lalwani
źródło
Ostatni (radians (lat) musi być sin (radians (lat))
KGs
pojawia się błąd „nieznana odległość kolumny” dlaczego?
Jill John,
@JillJohn, jeśli chcesz tylko odległości, możesz całkowicie usunąć zamówienie według odległości. Jeśli chcesz posortować wyniki, możesz użyć tego - 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
2

Myślałem, że moja implementacja javascript będzie dobrym odniesieniem do:

/*
 * 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
źródło
0

obliczyć odległość w Mysql

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

w ten sposób zostanie obliczona wartość odległości i każdy może złożyć wniosek zgodnie z wymaganiami.

Rajesh Prasad Yadav
źródło