Moyen le plus rapide de trouver la distance entre deux points lat / long
Question
J'ai actuellement un peu moins d'un million d'emplacements dans une base de données mysql, tous avec des informations de longitude et de latitude.
J'essaie de trouver la distance entre un point et de nombreux autres points via une requête. Ce n'est pas aussi rapide que je le souhaite, surtout avec plus de 100 coups par seconde.
Existe-t-il une requête plus rapide ou éventuellement un système plus rapide que mysql? J'utilise cette requête:
SELECT
name,
( 3959 * acos( cos( radians(42.290763) ) * cos( radians( locations.lat ) )
* cos( radians(locations.lng) - radians(-71.35368)) + sin(radians(42.290763))
* sin( radians(locations.lat)))) AS distance
FROM locations
WHERE active = 1
HAVING distance < 10
ORDER BY distance;
Remarque: la distance fournie est en Milles . Si vous avez besoin de kilomètres , utilisez 6371
au lieu de 3959
.
La solution
-
Créez vos points en utilisant les valeurs
Point
des types de donnéesGéométrie
de la tableMyISAM
. Depuis Mysql 5.7.5, les tablesInnoDB
prennent également en charge les indexSPATIAL
. -
Créez un index
SPATIAL
sur ces points -
Utilisez
MBRContains ()
pour rechercher les valeurs:SELECT * FROM table WHERE MBRContains(LineFromText(CONCAT( '(' , @lon + 10 / ( 111.1 / cos(RADIANS(@lon))) , ' ' , @lat + 10 / 111.1 , ',' , @lon - 10 / ( 111.1 / cos(RADIANS(@lat))) , ' ' , @lat - 10 / 111.1 , ')' ) ,mypoint)
, ou, dans MySQL 5.1
et supérieur:
SELECT *
FROM table
WHERE MBRContains
(
LineString
(
Point (
@lon + 10 / ( 111.1 / COS(RADIANS(@lat))),
@lat + 10 / 111.1
),
Point (
@lon - 10 / ( 111.1 / COS(RADIANS(@lat))),
@lat - 10 / 111.1
)
),
mypoint
)
Ceci sélectionnera tous les points approximativement dans la case (@ lat +/- 10 km, @lon +/- 10 km)
.
Ce n’est en réalité pas une boîte, mais un rectangle sphérique: segment lié à la latitude et la longitude de la sphère. Cela peut différer d’un rectangle simple sur le Franz Joseph Land , mais assez proche de celui-ci dans la plupart des lieux habités.
-
Appliquez un filtrage supplémentaire pour tout sélectionner à l'intérieur du cercle (et non du carré)
-
Appliquez éventuellement un filtrage fin supplémentaire pour tenir compte de la distance du grand cercle (pour les grandes distances)
Autres conseils
Ce n'est pas une réponse spécifique à MySql, mais cela améliorera les performances de votre requête SQL.
Ce que vous êtes en train de faire, c'est calculer la distance par rapport à chaque point du tableau pour voir si elle se trouve à moins de 10 unités d'un point donné.
Ce que vous pouvez faire avant d’exécuter ce sql, est de créer quatre points qui dessinent une boîte de 20 unités d’un côté, avec votre point au centre c’est-à-dire .. (x1, y1). . . (x4, y4), où (x1, y1) est (étant donné long + 10 unités, donnéLat + 10 unités). . . (donnéLong - 10 unités, donnéLat -10 unités). En fait, vous n'avez besoin que de deux points: en haut à gauche et en bas à droite, appelez-les (X1, Y1) et (X2, Y2)
Désormais, votre instruction SQL utilise ces points pour exclure les lignes dont la taille est supérieure à 10u de votre point donné. Elle peut également utiliser des index sur les latitudes. longitudes, les ordres de grandeur seront donc plus rapides que ce que vous avez actuellement.
par exemple
select . . .
where locations.lat between X1 and X2
and locations.Long between y1 and y2;
L'approche case peut renvoyer des faux positifs (vous pouvez récupérer des points situés dans les coins de la case qui sont> 10u à partir du point donné). Vous devez donc encore calculer la distance de chaque point. Cependant, cela sera à nouveau beaucoup plus rapide car vous avez considérablement limité le nombre de points à tester aux points situés dans la zone.
J’appelle cette technique "Penser à l’intérieur de la boîte" :)
MODIFIER: cela peut-il être inséré dans une instruction SQL?
Je n'ai aucune idée de ce que mySql ou Php est capable de faire, désolé. Je ne sais pas où le meilleur endroit est de construire les quatre points, ou comment ils pourraient être passés à une requête mySql en Php. Cependant, une fois que vous avez les quatre points, rien ne vous empêche de combiner votre propre instruction SQL avec la mienne.
select name,
( 3959 * acos( cos( radians(42.290763) )
* cos( radians( locations.lat ) )
* cos( radians( locations.lng ) - radians(-71.35368) )
+ sin( radians(42.290763) )
* sin( radians( locations.lat ) ) ) ) AS distance
from locations
where active = 1
and locations.lat between X1 and X2
and locations.Long between y1 and y2
having distance < 10 ORDER BY distance;
Je sais avec MS SQL que je peux créer une instruction SQL qui déclare quatre flottants (X1, Y1, X2, Y2) et les calcule avant le "principal". comme je l'ai dit, je n'ai aucune idée si cela peut être fait avec MySql. Cependant, je serais toujours enclin à construire les quatre points en C # et à les transmettre en tant que paramètres à la requête SQL.
Désolé, je ne peux pas vous aider davantage si quelqu'un peut répondre à MySQL & amp; Des parties spécifiques à cela, n'hésitez pas à modifier cette réponse pour le faire.
Vérifiez cette présentation pour une bonne réponse. Fondamentalement, il montre les deux approches différentes montrées dans les commentaires, avec une explication détaillée sur pourquoi / quand vous devriez utiliser l’une ou l’autre et pourquoi le "dans la boîte" le calcul peut être très intéressant.
sur cet article de blog , la fonction MySql suivante a été publiée. Je ne l'ai pas beaucoup testé, mais d'après ce que j'ai tiré de l'article, si vos champs de latitude et de longitude sont indexés , cela pourrait bien fonctionner pour vous:
DELIMITER $
DROP FUNCTION IF EXISTS `get_distance_in_miles_between_geo_locations` $
CREATE FUNCTION get_distance_in_miles_between_geo_locations(geo1_latitude decimal(10,6), geo1_longitude decimal(10,6), geo2_latitude decimal(10,6), geo2_longitude decimal(10,6))
returns decimal(10,3) DETERMINISTIC
BEGIN
return ((ACOS(SIN(geo1_latitude * PI() / 180) * SIN(geo2_latitude * PI() / 180) + COS(geo1_latitude * PI() / 180) * COS(geo2_latitude * PI() / 180) * COS((geo1_longitude - geo2_longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515);
END $
DELIMITER ;
Exemple d'utilisation: En supposant une table appelée Lieux avec des champs latitude & amp; longitude:
sélectionnez get_distance_in_miles_between_geo_locations (-34.017330, 22.809500, latitude, longitude) en tant que distance_de_passe depuis des lieux;
SELECT * FROM (SELECT *,(((acos(sin((43.6980168*pi()/180)) *
sin((latitude*pi()/180))+cos((43.6980168*pi()/180)) *
cos((latitude*pi()/180)) * cos(((7.266903899999988- longitude)*
pi()/180))))*180/pi())*60*1.1515 ) as distance
FROM wp_users WHERE 1 GROUP BY ID limit 0,10) as X
ORDER BY ID DESC
C’est la requête de calcul de distance entre les points de MySQL, je l’ai utilisée dans une longue base de données, ça marche parfaitement! Remarque: effectuez les modifications (nom de la base de données, nom de la table, colonne, etc.) selon vos besoins.
set @latitude=53.754842;
set @longitude=-2.708077;
set @radius=20;
set @lng_min = @longitude - @radius/abs(cos(radians(@latitude))*69);
set @lng_max = @longitude + @radius/abs(cos(radians(@latitude))*69);
set @lat_min = @latitude - (@radius/69);
set @lat_max = @latitude + (@radius/69);
SELECT * FROM postcode
WHERE (longitude BETWEEN @lng_min AND @lng_max)
AND (latitude BETWEEN @lat_min and @lat_max);
si vous utilisez MySQL 5.7. *, vous pouvez utiliser st_distance_sphere (POINT, POINT) .
Select st_distance_sphere(POINT(-2.997065, 53.404146 ), POINT(58.615349, 23.56676 ))/1000 as distcance
select
(((acos(sin(('$latitude'*pi()/180)) * sin((`lat`*pi()/180))+cos(('$latitude'*pi()/180))
* cos((`lat`*pi()/180)) * cos((('$longitude'- `lng`)*pi()/180))))*180/pi())*60*1.1515)
AS distance
from table having distance<22;
Le code complet contenant des informations détaillées sur l'installation d'un plug-in MySQL est disponible à l'adresse suivante: https://github.com/lucasepe / lib_mysqludf_haversine
J'ai posté cette dernière année en tant que commentaire. Depuis la gentillesse de @TylerCollier m'a suggéré de poster comme réponse, la voici.
Une autre méthode consiste à écrire une fonction UDF personnalisée qui renvoie la distance haversine entre deux points. Cette fonction peut prendre en entrée:
lat1 (real), lng1 (real), lat2 (real), lng2 (real), type (string - optinal - 'km', 'ft', 'mi')
Nous pouvons donc écrire quelque chose comme ceci:
SELECT id, name FROM MY_PLACES WHERE haversine_distance(lat1, lng1, lat2, lng2) < 40;
chercher tous les enregistrements avec une distance inférieure à 40 kilomètres. Ou:
SELECT id, name FROM MY_PLACES WHERE haversine_distance(lat1, lng1, lat2, lng2, 'ft') < 25;
pour récupérer tous les enregistrements dont la distance est inférieure à 25 pieds.
La fonction principale est:
double
haversine_distance( UDF_INIT* initid, UDF_ARGS* args, char* is_null, char *error ) {
double result = *(double*) initid->ptr;
/*Earth Radius in Kilometers.*/
double R = 6372.797560856;
double DEG_TO_RAD = M_PI/180.0;
double RAD_TO_DEG = 180.0/M_PI;
double lat1 = *(double*) args->args[0];
double lon1 = *(double*) args->args[1];
double lat2 = *(double*) args->args[2];
double lon2 = *(double*) args->args[3];
double dlon = (lon2 - lon1) * DEG_TO_RAD;
double dlat = (lat2 - lat1) * DEG_TO_RAD;
double a = pow(sin(dlat * 0.5),2) +
cos(lat1*DEG_TO_RAD) * cos(lat2*DEG_TO_RAD) * pow(sin(dlon * 0.5),2);
double c = 2.0 * atan2(sqrt(a), sqrt(1-a));
result = ( R * c );
/*
* If we have a 5th distance type argument...
*/
if (args->arg_count == 5) {
str_to_lowercase(args->args[4]);
if (strcmp(args->args[4], "ft") == 0) result *= 3280.8399;
if (strcmp(args->args[4], "mi") == 0) result *= 0.621371192;
}
return result;
}
Une approximation rapide, simple et précise (pour de plus petites distances) peut être réalisée avec une projection sphérique . Au moins dans mon algorithme de routage, j'obtiens une augmentation de 20% par rapport au calcul correct. En code Java, cela ressemble à:
public double approxDistKm(double fromLat, double fromLon, double toLat, double toLon) {
double dLat = Math.toRadians(toLat - fromLat);
double dLon = Math.toRadians(toLon - fromLon);
double tmp = Math.cos(Math.toRadians((fromLat + toLat) / 2)) * dLon;
double d = dLat * dLat + tmp * tmp;
return R * Math.sqrt(d);
}
Pas sûr de MySQL (désolé!).
Assurez-vous de connaître la limitation (le troisième paramètre d’assertEquals signifie l’exactitude en kilomètres):
float lat = 24.235f;
float lon = 47.234f;
CalcDistance dist = new CalcDistance();
double res = 15.051;
assertEquals(res, dist.calcDistKm(lat, lon, lat - 0.1, lon + 0.1), 1e-3);
assertEquals(res, dist.approxDistKm(lat, lon, lat - 0.1, lon + 0.1), 1e-3);
res = 150.748;
assertEquals(res, dist.calcDistKm(lat, lon, lat - 1, lon + 1), 1e-3);
assertEquals(res, dist.approxDistKm(lat, lon, lat - 1, lon + 1), 1e-2);
res = 1527.919;
assertEquals(res, dist.calcDistKm(lat, lon, lat - 10, lon + 10), 1e-3);
assertEquals(res, dist.approxDistKm(lat, lon, lat - 10, lon + 10), 10);
Voici une description très détaillée de Geo Distance Search avec MySQL, une solution basée sur la mise en œuvre de Haversine Formula dans mysql. La description complète de la solution avec théorie, implémentation et optimisation des performances. Bien que la partie optimisation spatiale ne fonctionne pas correctement dans mon cas. http://www.scribd.com/doc/2569355/Geo -Distance-Recherche-avec-MySQL
Prenez connaissance de la recherche de géolocalisation avec MySQL , une solution basé sur la mise en œuvre de Haversine Formula to MySQL. C'est une solution complète description avec théorie, mise en œuvre et optimisation ultérieure des performances. Bien que la partie optimisation spatiale ne fonctionne pas correctement dans mon cas.
J'ai remarqué deux erreurs dans ceci:
-
l'utilisation de
abs
dans l'instruction select sur p8. J'ai juste omisabs
et cela a fonctionné. -
la fonction de distance de recherche spatiale sur p27 ne convertit pas en radians ni ne multiplie la longitude par
cos (latitude)
, à moins que ses données spatiales ne soient chargées avec cela en considération (impossible à déterminer à partir du contexte de article), mais son exemple sur p26 indique que ses données spatialesPOINT
ne sont pas chargées en radians ou en degrés.
Une fonction MySQL qui renvoie le nombre de mètres entre les deux coordonnées:
CREATE FUNCTION DISTANCE_BETWEEN (lat1 DOUBLE, lon1 DOUBLE, lat2 DOUBLE, lon2 DOUBLE)
RETURNS DOUBLE DETERMINISTIC
RETURN ACOS( SIN(lat1*PI()/180)*SIN(lat2*PI()/180) + COS(lat1*PI()/180)*COS(lat2*PI()/180)*COS(lon2*PI()/180-lon1*PI()/180) ) * 6371000
Pour renvoyer la valeur dans un format différent, remplacez 6371000
dans la fonction par le rayon de la Terre dans votre choix d'unité. Par exemple, les kilomètres seraient 6371
et les miles, 3959
.
Pour utiliser cette fonction, appelez-la comme toute autre fonction de MySQL. Par exemple, si vous avez une table ville
, vous pouvez trouver la distance entre chaque ville et toutes les villes:
SELECT
`city1`.`name`,
`city2`.`name`,
ROUND(DISTANCE_BETWEEN(`city1`.`latitude`, `city1`.`longitude`, `city2`.`latitude`, `city2`.`longitude`)) AS `distance`
FROM
`city` AS `city1`
JOIN
`city` AS `city2`
Je devais résoudre un problème similaire (filtrer les lignes en fonction de la distance d'un point) et en combinant une question originale avec des réponses et des commentaires, je trouvais une solution parfaitement adaptée à MySQL 5.6 et 5.7.
SELECT
*,
(6371 * ACOS(COS(RADIANS(56.946285)) * COS(RADIANS(Y(coordinates)))
* COS(RADIANS(X(coordinates)) - RADIANS(24.105078)) + SIN(RADIANS(56.946285))
* SIN(RADIANS(Y(coordinates))))) AS distance
FROM places
WHERE MBRContains
(
LineString
(
Point (
24.105078 + 15 / (111.320 * COS(RADIANS(56.946285))),
56.946285 + 15 / 111.133
),
Point (
24.105078 - 15 / (111.320 * COS(RADIANS(56.946285))),
56.946285 - 15 / 111.133
)
),
coordinates
)
HAVING distance < 15
ORDER By distance
Les POINT
et ont un index SPATIAL
6371
sert à calculer la distance en kilomètres
56.946285
est la latitude du point central
24.105078
est la longitude du point central
10
est la distance maximale en kilomètres
Dans mes tests, MySQL utilise SPATIAL index sur le champ coordonnées
pour sélectionner rapidement toutes les lignes situées dans un rectangle, puis calcule la distance réelle de tous les emplacements filtrés afin d'exclure des emplacements des coins de rectangles et de ne laisser que des emplacements à l'intérieur. cercle.
Ceci est la visualisation de mon résultat:
Les étoiles grises visualisent tous les points de la carte, les étoiles jaunes sont celles renvoyées par la requête MySQL. Les étoiles grises situées à l'intérieur des coins du rectangle (mais hors du cercle) ont été sélectionnées par MBRContains ()
, puis désélectionnées par la clause HAVING
.
Utiliser mysql
SET @orig_lon = 1.027125;
SET @dest_lon = 1.027125;
SET @orig_lat = 2.398441;
SET @dest_lat = 2.398441;
SET @kmormiles = 6371;-- for distance in miles set to : 3956
SELECT @kmormiles * ACOS(LEAST(COS(RADIANS(@orig_lat)) *
COS(RADIANS(@dest_lat)) * COS(RADIANS(@orig_lon - @dest_lon)) +
SIN(RADIANS(@orig_lat)) * SIN(RADIANS(@dest_lat)),1.0)) as distance;
Voir: https://andrew.hedges.name/experiments/haversine/
Voir: https://stackoverflow.com/a/24372831/5155484
Voir: http://www.plumislandmedia.net/mysql/ haversine-mysql-most-loc /
REMARQUE: LEAST
est utilisé pour éviter les valeurs null en tant que commentaire suggéré dans https: // stackoverflow. com / a / 24372831/5155484
$objectQuery = "SELECT table_master.*, ((acos(sin((" . $latitude . "*pi()/180)) * sin((`latitude`*pi()/180))+cos((" . $latitude . "*pi()/180)) * cos((`latitude`*pi()/180)) * cos(((" . $longitude . "- `longtude`)* pi()/180))))*180/pi())*60*1.1515 as distance FROM `table_post_broadcasts` JOIN table_master ON table_post_broadcasts.master_id = table_master.id WHERE table_master.type_of_post ='type' HAVING distance <='" . $Radius . "' ORDER BY distance asc";