MySQL Great Circle Distance (Haversine-Formel)

184

Ich habe ein funktionierendes PHP-Skript, das Longitude- und Latitude-Werte abruft und diese dann in eine MySQL-Abfrage eingibt. Ich möchte es nur MySQL machen. Hier ist mein aktueller PHP-Code:

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

Weiß jemand, wie man dies vollständig zu MySQL macht? Ich habe ein bisschen im Internet gestöbert, aber die meiste Literatur darüber ist ziemlich verwirrend.

Nick Woodhams
quelle
4
Basierend auf all den hervorragenden Antworten unten, hier ist ein Arbeitsbeispiel der Haversine-Formel in Aktion
StartupGuy
Vielen Dank für das Teilen, dass Michael.M
Nick Woodhams
stackoverflow.com/a/40272394/1281385 Hat ein Beispiel, wie man sicherstellt, dass der Index getroffen wird
Exussum

Antworten:

357

Aus den häufig gestellten Fragen zu Google Code - Erstellen eines Store Locator mit PHP, MySQL und Google Maps :

Hier ist die SQL-Anweisung, die die nächsten 20 Positionen findet, die sich in einem Radius von 25 Meilen zur 37, -122-Koordinate befinden. Es berechnet die Entfernung basierend auf dem Breiten- / Längengrad dieser Zeile und dem Zielbreiten- / Längengrad und fragt dann nur nach Zeilen, bei denen der Entfernungswert weniger als 25 beträgt, ordnet die gesamte Abfrage nach Entfernung und begrenzt sie auf 20 Ergebnisse. Ersetzen Sie 3959 durch 6371, um nach Kilometern anstatt nach Meilen zu suchen.

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
quelle
2
Die SQL-Anweisung ist wirklich gut. aber wo kann ich meine Koordinaten in diese Aussage eingeben? Ich kann nicht sehen, wo Koordinaten vergangen sind
Mann
32
Ersetzen Sie 37 und -122 durch Ihre Koordinaten.
Pavel Chuchuva
5
Ich frage mich über die Auswirkungen auf die Leistung, wenn es Millionen von Orten (+ Tausende von Besuchern) gibt ...
Halil Özgür
12
Sie können die Abfrage für eine bessere Leistung eingrenzen,
maliayas
2
@FosAvance Ja, diese Abfrage würde funktionieren, wenn Sie eine markersTabelle mit den Feldern id, lan und lng haben.
Pavel Chuchuva
32

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

mit Längen- und Breitengrad im Bogenmaß.

so

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

ist Ihre SQL-Abfrage

Um Ihre Ergebnisse in km oder Meilen zu erhalten, multiplizieren Sie das Ergebnis mit dem mittleren Radius der Erde ( 3959Meilen, 6371km oder 3440Seemeilen).

Das, was Sie in Ihrem Beispiel berechnen, ist ein Begrenzungsrahmen. Wenn Sie Ihre Koordinatendaten in eine räumlich aktivierte MySQL-Spalte einfügen , können Sie die integrierten Funktionen von MySQL verwenden, um die Daten abzufragen.

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

Wenn Sie der Koordinatentabelle Hilfsfelder hinzufügen, können Sie die Antwortzeit der Abfrage verbessern.

So was:

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

Wenn Sie TokuDB verwenden, erzielen Sie eine noch bessere Leistung, wenn Sie Clustering-Indizes für eines der Prädikate hinzufügen, z. B.:

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

Sie benötigen für jeden Punkt das Basislat und Lon in Grad sowie sin (lat) im Bogenmaß, cos (lat) * cos (lon) im Bogenmaß und cos (lat) * sin (lon) im Bogenmaß. Dann erstellen Sie eine MySQL-Funktion, wie folgt:

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

Dies gibt Ihnen die Entfernung.

Vergessen Sie nicht, einen Index für lat / lon hinzuzufügen, damit das Begrenzungsfeld die Suche unterstützen kann, anstatt es zu verlangsamen (der Index wurde bereits in der obigen Abfrage CREATE TABLE hinzugefügt).

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

Bei einer alten Tabelle mit nur Lat / Lon-Koordinaten können Sie ein Skript einrichten, um es folgendermaßen zu aktualisieren: (PHP mit 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']
  ));
}

Anschließend optimieren Sie die eigentliche Abfrage so, dass die Entfernungsberechnung nur dann durchgeführt wird, wenn sie wirklich benötigt wird, indem Sie beispielsweise den Kreis (gut, oval) von innen und außen begrenzen. Dazu müssen Sie mehrere Metriken für die Abfrage selbst vorberechnen:

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

In Anbetracht dieser Vorbereitungen geht die Abfrage ungefähr so ​​(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 in der obigen Abfrage könnte bedeuten, dass kein Index verwendet wird, es sei denn, es gibt genügend Ergebnisse, um einen solchen auszulösen. Der Index wird verwendet, wenn die Koordinatentabelle genügend Daten enthält. Sie können FORCE INDEX (lat_lon_idx) zum SELECT hinzufügen, damit es den Index ohne Rücksicht auf die Tabellengröße verwendet, sodass Sie mit EXPLAIN überprüfen können, ob er ordnungsgemäß funktioniert.

Mit den obigen Codebeispielen sollten Sie eine funktionierende und skalierbare Implementierung der Objektsuche nach Entfernung mit minimalem Fehler haben.

silvio
quelle
10

Ich musste das etwas genauer ausarbeiten, also werde ich mein Ergebnis teilen. Dies verwendet eine zipTabelle mit latitudeund longitudeTabellen. Es hängt nicht von Google Maps ab. Vielmehr können Sie es an jede Tabelle anpassen, die lat / long enthält.

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

Sehen Sie sich diese Zeile in der Mitte dieser Abfrage an:

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

Dies sucht nach den 30 nächsten Einträgen in der zipTabelle innerhalb von 50,0 Meilen vom Lat / Long-Punkt 42.81 / -70.81. Wenn Sie dies in eine App einbauen, geben Sie dort Ihren eigenen Punkt und Suchradius ein.

Wenn Sie in Kilometern anstatt in Meilen arbeiten möchten, wechseln Sie in der Abfrage 69zu 111.045und 3963.17zu 6378.10.

Hier ist eine detaillierte Beschreibung. Ich hoffe es hilft jemandem. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

O. Jones
quelle
3

Ich habe eine Prozedur geschrieben, die dasselbe berechnen kann, aber Sie müssen den Breiten- und Längengrad in die entsprechende Tabelle eingeben.

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
quelle
3

Ich kann die obige Antwort nicht kommentieren, aber sei vorsichtig mit der Antwort von @Pavel Chuchuva. Diese Formel gibt kein Ergebnis zurück, wenn beide Koordinaten gleich sind. In diesem Fall ist der Abstand null, sodass diese Zeile nicht mit dieser Formel zurückgegeben wird.

Ich bin kein MySQL-Experte, aber das scheint für mich zu funktionieren:

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
quelle
2
Wenn die Positionen identisch sind, sollte sie nicht NULL sein, sondern Null (wie ACOS(1)0). Sie könnten sehen Probleme mit der x - Achse * xaxis + yaxis * yaxis + zaxis * zaxis gehen außerhalb des Bereichs für ACOS Runden , aber sie scheinen nicht gegen die Bewachung zu werden?
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 

für eine Entfernung von 25 km

Harish Lalwani
quelle
Das letzte (Bogenmaß (lat) muss Sünde sein (Bogenmaß (lat))
KGs
Ich bekomme eine Fehlermeldung "Unbekannter Spaltenabstand" Warum ist das so?
Jill John
@JillJohn Wenn Sie nur Entfernung möchten, können Sie die Reihenfolge nach Entfernung vollständig entfernen. Wenn Sie Ergebnisse sortieren möchten, können Sie Folgendes verwenden: ORDER BY (6371 * acos (cos (Bogenmaß (search_lat)) * cos (Bogenmaß (lat)) * cos (Bogenmaß (lng) - Bogenmaß (search_lng)) + sin (Bogenmaß) (search_lat)) * sin (Bogenmaß (lat)))).
Harish Lalwani
2

Ich dachte, meine Javascript-Implementierung wäre ein guter Hinweis auf:

/*
 * 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
quelle
0

Berechnen Sie die Entfernung in MySQL

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

Somit wird der Entfernungswert berechnet und jeder kann sich nach Bedarf bewerben.

Rajesh Prasad Yadav
quelle