MySQL Great Circle Distance (fórmula de Haversine)

184

Tengo un script PHP en funcionamiento que obtiene valores de longitud y latitud y luego los ingresa en una consulta MySQL. Me gustaría hacerlo ú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 un poco por Internet, pero la mayor parte de la literatura es bastante confusa.

Nick Woodhams
fuente
44
Basado en todas las excelentes respuestas a continuación, aquí hay una muestra funcional de la fórmula de Haversine en acción
StartupGuy el
Gracias por compartir eso Michael.M
Nick Woodhams
stackoverflow.com/a/40272394/1281385 Tiene un ejemplo de cómo asegurarse de que el índice sea alcanzado
exussum

Respuestas:

357

Desde Google Code FAQ - Crear un localizador de tiendas con PHP, MySQL y Google Maps :

Aquí está la instrucción SQL que encontrará las 20 ubicaciones más cercanas que están dentro de un radio de 25 millas a la coordenada 37, -122. 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;
Pavel Chuchuva
fuente
2
La declaración SQL es realmente buena. pero ¿dónde puedo pasar mis coordenadas en esta declaración? No puedo ver donde hayan pasado las coordenadas
Mann
32
Reemplaza 37 y -122 con tus coordenadas.
Pavel Chuchuva
55
Me pregunto sobre las implicaciones de rendimiento de esto si hay millones de lugares (+ miles de visitantes) ...
Halil Özgür
12
Puede limitar la consulta para un mejor rendimiento como se explica en este documento: tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
maliayas
2
@FosAvance Sí, esta consulta funcionaría si tiene una markerstabla con los campos id, lan y lng.
Pavel Chuchuva
32

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

con latitud y longitud en radianes.

entonces

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

es tu consulta SQL

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

Lo que está calculando en su ejemplo es un cuadro delimitador. Si coloca sus datos de coordenadas en una columna MySQL espacial habilitada , puede usar la funcionalidad integrada de MySQL para consultar los datos.

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

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

Me gusta esto:

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 agrega índices de agrupación 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á los lat y lon básicos en grados, así como sin (lat) en radianes, cos (lat) * cos (lon) en radianes y cos (lat) * sin (lon) en radianes para cada punto. Luego creas una función mysql, algo así:

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 te da la distancia.

No olvide agregar un índice en lat / lon para que el cuadro delimitador pueda ayudar a la búsqueda en lugar de ralentizarlo (el índice ya se agregó en la consulta CREAR TABLA anterior).

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

Dada una tabla antigua con solo coordenadas lat / lon, puede configurar una secuencia de comandos para actualizarla así: (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']
  ));
}

Luego, optimiza la consulta real para hacer solo el cálculo de la distancia cuando realmente se necesita, por ejemplo, delimitando el círculo (bueno, ovalado) desde adentro y desde afuera. Para eso, deberá calcular previamente varias métricas para la consulta en sí:

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

Dados esos preparativos, 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);

EXPLICAR en la consulta anterior podría decir que no está usando el índice a menos que haya suficientes resultados para activarlo. El índice se usará cuando haya suficientes datos en la tabla de coordenadas. Puede agregar FORCE INDEX (lat_lon_idx) a SELECT para que use el índice sin tener en cuenta el tamaño de la tabla, para que pueda verificar con EXPLAIN que funciona 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.

silvio
fuente
10

He tenido que resolver esto con cierto detalle, así que compartiré mi resultado. Esto usa una ziptabla con latitudey longitudetablas. No depende de Google Maps; más bien puede 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

Mire esta línea en el medio de esa consulta:

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

Esto busca las 30 entradas más cercanas en la ziptabla dentro de 50.0 millas del punto lat / long 42.81 / -70.81. Cuando construyes esto en una aplicación, ahí es donde pones tu propio punto y radio de búsqueda.

Si desea trabajar en kilómetros en lugar de millas, el cambio 69a 111.045y el cambio 3963.17a6378.10 la consulta.

Aquí hay una reseña detallada. Espero que ayude a alguien. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

O. Jones
fuente
3

He escrito un procedimiento que puede calcular lo mismo, pero debe ingresar la latitud y la 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 ;
Abdul Manaf
fuente
3

No puedo comentar sobre la respuesta anterior, pero tenga 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 tal 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;
John Crenshaw
fuente
2
Si las posiciones son idénticas, no debería salir NULL, sino como cero (como ACOS(1)es 0). Es posible que vea problemas de redondeo con el xaxis * xaxis + yaxis * yaxis + zaxis * zaxis fuera de rango para ACOS, pero ¿no parece estar evitando eso?
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 

para una distancia de 25 km

Harish Lalwani
fuente
El último (radianes (lat) debe ser sin (radianes (lat))
KGs
recibo un error "distancia de columna desconocida" ¿por qué es esto?
Jill John
@JillJohn si solo quieres distancia, puedes eliminar el orden por distancia por completo Si desea ordenar los resultados, puede usar esto: ORDER BY (6371 * acos (cos (radianes (search_lat)) * cos (radianes (lat)) * cos (radianes (lng) - radianes (search_lng)) + sin (radianes (search_lat)) * sin (radianes (lat)))).
Harish Lalwani
2

Pensé que mi implementación de JavaScript sería una buena referencia para:

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

calcular la distancia en Mysql

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

por lo tanto, se calculará el valor de distancia y cualquiera puede solicitarlo según sea necesario

Rajesh Prasad Yadav
fuente