MySQL Great Circle Distance (fórmula Haversine)


168

Tengo un script PHP que funciona y obtiene los valores de longitud y latitud y luego los ingresa en una consulta MySQL. Me gustaría que sea únicamente MySQL. Aquí está mi Código PHP actual:

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

¿Alguien sabe cómo hacer esto completamente MySQL? He navegado por Internet un poco, pero la mayoría de la literatura es bastante confusa.

+4

Basado en todas las excelentes respuestas a continuación, [muestra aquí está trabajando de la fórmula Haversine en acción] (http://sqlfiddle.com/#!2/abba1/2/0) 19 ene. 142014-01-19 06:29:13

  0

Gracias por compartir que Michael.M 19 ene. 142014-01-19 11:03:31

  0

http://stackoverflow.com/a/40272394/1281385 Tiene un ejemplo de cómo asegurarse de que el índice sea golpeado 26 oct. 162016-10-26 21:32:09

311

De Google Code FAQ - Creating a Store Locator with PHP, MySQL & Google Maps:

Aquí está la instrucción SQL que se encuentran los 20 lugares más cercanos que se encuentran dentro de un radio de 25 millas al 37, -122 coordenadas. Calcula la distancia en función de la latitud/longitud de esa fila y la latitud/longitud objetivo, y luego solicita solo filas donde el valor de la distancia es inferior a 25, ordena toda la consulta por distancia y la limita a 20 resultados. Para buscar por kilómetros en lugar de millas, reemplace 3959 con 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

Exactamente lo que necesitaba, gracias. 11 may. 112011-05-11 18:56:07

+2

la declaración SQL es realmente buena. pero ¿dónde puedo pasar mis coordenadas en esta declaración? no puedo ver en ninguna parte las coordenadas han pasado 13 ago. 112011-08-13 14:43:40

+28

Reemplace 37 y -122 con sus coordenadas. 14 ago. 112011-08-14 05:32:53

+4

Me pregunto acerca de las implicaciones de rendimiento de esto si hay millones de lugares (+ miles de visitantes) ... 01 dic. 112011-12-01 10:26:08

  0

el almacenamiento en caché siempre es su amigo en esa circunstancia 21 feb. 122012-02-21 22:13:58

+11

Puede restringir la consulta para un mejor rendimiento como se explica en este documento: http : //tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL 15 nov. 122012-11-15 01:25:40

  0

Excelente artículo, gracias @maliayas 11 jul. 132013-07-11 16:40:17

  0

-1 por no reemplazar las constantes con las variables lat & lon 12 mar. 142014-03-12 08:27:13

  0

son "lat" y "lng" "campos en la base de datos? 29 mar. 142014-03-29 16:49:03

+2

@FosAvance Sí, esta consulta funcionaría si tiene la tabla 'marcadores' con los campos id, lan y lng. 29 mar. 142014-03-29 23:08:31

  0

hablando de rendimiento, podría alguien decirme si la versión de Google es mejor que ésta: __ ((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 La fórmula a la que se refiere es la ley esférica de los cosenos y es casi idéntica a la fórmula de Haversine. La fórmula de la ley esférica de coseno devuelve exactamente el mismo resultado que la fórmula de Haversine para distancias superiores a unos pocos metros, y es dos veces más rápida. 10 abr. 152015-04-10 14:56:25

  0

Quiero agregar SQL LIKE consulta en esto.? 27 oct. 152015-10-27 09:00:37

  0

Esta fórmula tiene errores. Utilicé uno muy similar (en kilómetros) y si un punto está exactamente en el centro del círculo (radio), este punto no se seleccionará. Estoy luchando para resolver este error, compruebe esto -> http://stackoverflow.com/questions/42522005/select-points-from-map-database-according-to-radius 01 mar. 172017-03-01 03:34:31

  0

Ahora hay un error en la página original de Google. El fragmento de código todavía tiene 37 y -122 como lat/lng en la consulta, pero el texto ahora dice "Aquí está la declaración SQL que encuentra las 20 ubicaciones más cercanas dentro de un radio de 25 millas a la coordenada -33, 151" - esto hace es bastante confuso! 07 jun. 172017-06-07 12:48:06


28

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

con la latitud y longitud en radianes.

por lo

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

es la consulta SQL

para obtener sus resultados en km o millas, multiplica el resultado con el radio medio de la Tierra (3959 millas, 6371 Km o 3440 millas náuticas)

Lo que está calculando en su ejemplo es un cuadro delimitador. Si coloca sus datos de coordenadas en spatial enabled MySQL column, puede usar MySQL's build in functionality para consultar los datos.

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

3

He escrito un procedimiento que puede calcular la misma, pero hay que introducir la latitud y longitud en la tabla respectiva.

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

Si agrega campos auxiliares a la tabla de coordenadas, puede mejorar el tiempo de respuesta de la consulta.

De esta manera:

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

Si está utilizando tokudb, obtendrá un rendimiento aún mejor si se agrega la agrupación índices en cualquiera de los predicados, por ejemplo, así:

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

Necesitará el lat básico y el lon en grados, así como sen (lat) en radianes, cos (lat) * cos (lon) en radianes y cos (lat) * sen (lon) en radianes para cada punto . a continuación, crear una función de MySQL, smth como esto:

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 

Esto le da la distancia.

No se olvide de añadir un índice en latitud/longitud por lo que el boxeo de delimitación puede ayudar a la búsqueda en lugar de frenarla (el índice ya se ha agregado en la consulta CREATE TABLE arriba).

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

dado una mesa de edad con sólo coordenadas lat/lon, se puede configurar una secuencia de comandos para actualizar esta manera: (PHP usando 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'] 
)); 
} 

A continuación, optimizar la consulta real a solamente haga el cálculo de distancia cuando realmente lo necesite, por ejemplo, delimitando el círculo (bien, ovalado) desde dentro y desde fuera. Para ello, tendrá que precalcular varias métricas para la propia consulta:

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

Dadas estas preparaciones, la consulta es algo como esto (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); 

explique en la consulta anterior podría decir que no está usando índice a menos que haya suficientes resultados para desencadenarlo. El índice se usará cuando haya suficientes datos en la tabla de coordenadas. Puede agregar FORCE INDEX (lat_lon_idx) al SELECT para usar el índice sin tener en cuenta el tamaño de la tabla, por lo que puede verificar con EXPLAIN que está funcionando correctamente.

Con los ejemplos de código anteriores, debe tener una implementación funcional y escalable de búsqueda de objetos por distancia con un error mínimo.


2

pensé que mi aplicación Javascript sería una buena referencia a:

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

No puedo hacer ningún comentario sobre la respuesta anterior, pero ten cuidado con la respuesta de @Pavel Chuchuva. Esa fórmula no devolverá un resultado si ambas coordenadas son iguales. En ese caso, la distancia es nula, por lo que esa fila no se devolverá con esa fórmula como está.

no soy un experto en MySQL, pero esto parece estar funcionando para mí:

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

Si las posiciones son idénticas, * no deberían * salir NULL, sino como cero (como 'ACOS (1)' es 0). Usted * podría * ver problemas de redondeo con el xaxis * xaxis + yaxis * yaxis + zaxis * zaxis fuera de rango para ACOS pero no parece estar protegiendo contra eso? 15 mar. 132013-03-15 13:18:47


8

he tenido que resolver esto con cierto detalle, así que voy a compartir mi resultado. Esto usa una tabla zip con tablas latitude y longitude. No depende de Google Maps; más bien puedes adaptarlo a cualquier tabla que contenga 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 

mirada a esta línea en el medio de esa consulta:

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

esto busca al 30 entradas más cercanas en la tabla zip dentro de 50,0 millas del punto Lat/Long 42.81/-70.81. Cuando construyes esto en una aplicación, ahí es donde colocas tu propio radio de búsqueda y punto.

Si quieres trabajar en kilómetros en lugar de millas, cambiar 69 a 111.045 y cambiar 3963.17 a 6378.10 en la consulta.

Aquí hay una descripción detallada. Espero que ayude a alguien.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 

para la distancia de 25 kilometros

  0

El último (radianes (lat) debe ser sin (radianes (lat)) 08 mar. 172017-03-08 20:01:25

  0

@ KGs gracias. Editado según su sugerencia. 08 mar. 182018-03-08 14:33:10