Moyen le plus rapide pour trouver la distance entre deux points lat / long


227

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.

Y a-t-il une requête plus rapide ou peut-être un système plus rapide autre que mysql pour cela? 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 miles . Si vous avez besoin de kilomètres , utilisez 6371plutôt 3959.


31
La formule que vous donnez semble avoir beaucoup d'éléments constants. Est-il possible de pré-calculer des données et de stocker ces valeurs également dans votre base de données? Par exemple, 3959 * acos (cos (radians (42.290763)) est une constante mais contient 4 calculs majeurs. Au lieu de cela, pourriez-vous simplement stocker 6696.7837?
Peter M

1
Ou au moins pré-calculer des constantes en dehors de la requête? Cela réduira le travail qui doit être fait.
Peter M

2
@Peter M Il semble probable que toute base de données SQL décente soit optimisée, de sorte qu'elle n'a été calculée qu'une seule fois.
mhenry1384

25
Pour ceux qui se demandent, 42,290763 est la latitude et -71,35368 est la longitude du point à partir duquel calculer les distances.
user276648

14
Juste pour info, la distance calculée par cette formule est en miles, pas en kilomètres.S'il vous plaît remplacer 3959 à 6371 pour obtenir des résultats en kilomètres
Sahil

Réponses:


115
  • Créez vos points en utilisant les Pointvaleurs des Geometrytypes de données dans le MyISAMtableau. Depuis Mysql 5.7.5, les InnoDBtableaux prennent désormais également en charge les SPATIALindices.

  • Créer un SPATIALindex sur ces points

  • Utilisez MBRContains()pour trouver 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.1et au-dessus:

    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
                    )

Cela sélectionnera tous les points approximativement dans la boîte (@lat +/- 10 km, @lon +/- 10km).

Ce n'est en fait 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 la Terre François-Joseph , mais assez proche de lui dans la plupart des endroits habités.

  • Appliquer un filtrage supplémentaire pour sélectionner tout à l'intérieur du cercle (pas le carré)

  • Appliquer éventuellement un filtrage fin supplémentaire pour tenir compte de la distance du grand cercle (pour les grandes distances)


15
@Quassnoi: Quelques corrections: vous voudrez probablement changer l'ordre des coordonnées en lat, long. De plus, les distances longitudinales sont proportionnelles au cosinus de la latitude et non à la longitude. Et vous voudrez le changer de multiplication en division, donc votre première coordonnée sera corrigée comme @lon - 10 / ( 111.1 / cos(@lat))(et sera la deuxième de la paire une fois que tout sera correct.
M. Dave Auayan

8
AVERTISSEMENT : le corps de la réponse n'a PAS été modifié pour correspondre au commentaire très valable fait par @M. Dave Auayan. Notes supplémentaires: Cette méthode est en forme de poire si le cercle d'intérêt (a) comprend un pôle ou (b) est intersecté par le méridien de longitude à +/- 180 degrés. L'utilisation cos(lon)n'est également précise que pour les petites distances. Voir janmatuschek.de/LatitudeLongitudeBoundingCoordinates
John Machin

3
Existe-t-il un moyen de mieux comprendre ce que les constantes (10, 111.11, @lat, @lon, mypoint) représentent? Je suppose que le 10 est pour la distance en kilomètres, @lat et @lon représentent la longitude et la longitude fournies, mais que représentent 111.11 et mypoint dans l'exemple?
ashays

4
@ashays: il y a environ 111.(1)km dans un degré de latitude. mypointest le champ de la table qui stocke les coordonnées.
Quassnoi

1
Une autre correction d'erreur - il vous manque une fermeture) sur l'avant-dernière ligne
ina

100

Pas une réponse spécifique à MySql, mais cela améliorera les performances de votre instruction sql.

Ce que vous faites effectivement, c'est de calculer la distance à chaque point du tableau, pour voir si elle est à moins de 10 unités d'un point donné.

Ce que vous pouvez faire avant d'exécuter ce sql, c'est créer quatre points qui dessinent une boîte de 20 unités sur 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, étant donné Lat + 10 unités). . . (givenLong - 10units, givenLat -10 units). En fait, vous n'avez besoin que de deux points, en haut à gauche et en bas à droite, appelez-les (X1, Y1) et (X2, Y2)

Maintenant, votre instruction SQL utilise ces points pour exclure les lignes qui sont définitivement à plus de 10u de votre point donné, elle peut utiliser des index sur les latitudes et les longitudes, les ordres de grandeur seront donc plus rapides que ceux que vous avez actuellement.

par exemple

select . . . 
where locations.lat between X1 and X2 
and   locations.Long between y1 and y2;

L'approche boîte peut renvoyer des faux positifs (vous pouvez ramasser des points dans les coins de la boîte qui sont> 10u à partir du point donné), vous devez donc toujours calculer la distance de chaque point. Cependant, cela sera encore plus rapide car vous avez considérablement limité le nombre de points à tester aux points dans la boîte.

J'appelle cette technique "Penser à l'intérieur de la boîte" :)

EDIT: Cela peut-il être mis dans une seule instruction SQL?

Je n'ai aucune idée de ce dont mySql ou Php est capable, désolé. Je ne sais pas où le meilleur endroit est de construire les quatre points, ni comment ils pourraient être passés à une requête mySql dans 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 qu'avec MS SQL je peux construire une instruction SQL qui déclare quatre flottants (X1, Y1, X2, Y2) et les calcule avant l'instruction de sélection "principale", comme je l'ai dit, je ne sais pas 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 être plus d'aide, si quelqu'un peut répondre aux parties spécifiques de MySQL et Php, n'hésitez pas à modifier cette réponse pour le faire.


4
Vous pouvez trouver une procédure mysql pour cette approche dans cette présentation: scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
Lucia

37
Pour rechercher par kilomètres au lieu de miles, remplacez 3959 par 6371.
ErichBSchulz

4
+1, excellente option; l'ajout de la boîte a réduit ma requête de 4s à 0,03s en moyenne.
jvenema

1
Même si cela semble logique, vous réservez un prix pour cette solution! Sur une base de données d'enregistrement de 2 millions de dollars, la requête est passée de 16 secondes à 0,06 seconde. Remarque: C'est encore plus rapide (pour les grands tableaux) si vous supprimez le calcul de la distance de la requête et effectuez le calcul de la distance dans votre code de programme!
NLAnaconda

2
@Binary Worrier: Donc, les X1, X2 et Y1, Y2 seront Longitude Min et Max et Latitude Min et Max selon l'exemple donné ici: blog.fedecarg.com/2009/02/08/… s'il vous plaît aviser.
Prabhat

14

La fonction MySQL suivante a été publiée sur ce billet de blog . Je ne l'ai pas beaucoup testé, mais d'après ce que j'ai rassemblé du post, si vos champs de latitude et de longitude sont indexés , cela peut 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 placesavec champs latitude& longitude:

SELECT get_distance_in_miles_between_geo_locations(-34.017330, 22.809500,
latitude, longitude) AS distance_from_input FROM places;

J'ai essayé cela et cela fonctionne parfaitement, mais d'une certaine manière, cela ne me permet pas de mettre une instruction WHERE basée sur distance_from_input. Une idée pourquoi pas?
Chris Visser

vous pouvez le faire en tant que sous-sélection: sélectionnez * à partir de (...) comme t où distance_from_input> 5;
Brad Parks

2
ou allez tout simplement avec: sélectionnez * des endroits où get_distance_in_miles_between_geo_locations (-34.017330, 22.809500, latitude, longitude)> 5000;
Brad Parks

2
Mètres de retour:SELECT ROUND(((ACOS(SIN(lat1 * PI() / 180) * SIN(lat2 * PI() / 180) + COS(lat1 * PI() / 180) * COS(lat2 * PI() / 180) * COS((lnt1 - lnt2) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) * 1.609344 * 1000) AS distance
Mohammad

13

J'avais besoin de résoudre un problème similaire (filtrer les lignes en fonction de la distance à partir d'un seul point) et en combinant la question d'origine avec les réponses et les commentaires, j'ai trouvé une solution qui fonctionne parfaitement pour moi sur 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

coordinatesest un champ de type POINTet a un SPATIALindex
6371est pour le calcul de la distance en kilomètres
56.946285est la latitude pour le point central
24.105078est la longitude pour le point central
15est la distance maximale en kilomètres

Dans mes tests, MySQL utilise l'index SPATIAL sur le coordinateschamp pour sélectionner rapidement toutes les lignes qui se trouvent dans un rectangle, puis calcule la distance réelle pour tous les endroits filtrés pour exclure les endroits des coins des rectangles et ne laisser que des endroits à l'intérieur du cercle.

Voici la visualisation de mon résultat:

carte

Les étoiles grises visualisent tous les points sur la carte, les étoiles jaunes sont celles retournées par la requête MySQL. Les étoiles grises à l'intérieur des coins du rectangle (mais à l'extérieur du cercle) ont été sélectionnées MBRContains()et désélectionnées par la HAVINGclause.


Je ne peux pas voter assez. En parcourant une table avec environ 5 millions d'enregistrements et un index spatial avec cette méthode, le temps de recherche est de 0,005 seconde sur un ancien processeur A8. Je sais que 6371 peut être remplacé par 3959 pour obtenir des résultats en miles, mais les valeurs de 111.133 et 111.320 doivent-elles être ajustées ou sont-elles universellement constantes?
Wranorn

Excellente solution.
SeaBiscuit

Comment créer Point is it POINT (lat, lng) ou POINT (lng, lat)
user606669

2
@ user606669 It's POINT (lng, lat)
Māris Kiseļovs

Les fonctions X () et Y () devraient être ST_Y et ST_X de nos jours.
Andreas

11

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

1
c'est une très bonne alternative et facile à lire. gardez à l'esprit, l'ordre des paramètres à POINT () est (lng, lat) sinon vous pourriez vous retrouver avec des résultats "proches" mais toujours très différents des autres méthodes ici. voir: stackoverflow.com/questions/35939853/…
Andy P

9
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

Ceci est la requête de calcul de distance entre des points dans MySQL, je l'ai utilisée dans une longue base de données, cela fonctionne parfaitement! Remarque: effectuez les modifications (nom de la base de données, nom de la table, colonne, etc.) selon vos besoins.


Que représente la valeur 1,1515? J'ai déjà vu une formule similaire, mais elle utilisait 1,75 au lieu de 1,1515.
TryHarder

1
En réponse à ma propre question, je pense que la réponse pourrait se trouver ici stackoverflow.com/a/389251/691053
TryHarder

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

la source


11
Veuillez citer vos sources. Cela vient de: blog.fedecarg.com/2009/02/08/…
redburn

Qu'est-ce que 69 dans ce cas? Comment faire si on a le rayon de la terre?
CodeRunner

2
Kilomètre en 1 Latittude est de 111 KM. Mile in 1 Latittude est de 69 miles. et 69 milles = 111 kilomètres. C'est pourquoi nous avons utilisé les paramètres dans les conversions.
CodeRunner

Je cherchais ça depuis toujours. Je ne savais pas que cela pouvait être aussi simple. Merci beaucoup.
Vikas

Cela ne serait-il pas incorrect car lng_min / lng_max devrait utiliser lat_min et lat_max dans le calcul du rayon?
Ben

6
   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;

5

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 le 6371000dans la fonction par le rayon de la Terre dans l'unité de votre choix. Par exemple, les kilomètres seraient 6371et les milles seraient 3959.

Pour utiliser la fonction, il suffit de l'appeler comme vous le feriez pour n'importe quelle autre fonction dans MySQL. Par exemple, si vous aviez une table city, vous pourriez trouver la distance entre chaque ville et toutes les autres:

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`

4

Le code complet avec des détails sur l'installation en tant que plugin MySQL est ici: https://github.com/lucasepe/lib_mysqludf_haversine

J'ai posté cela l'année dernière en tant que commentaire. Puisque gentiment @TylerCollier m'a suggéré de poster comme réponse, le voici.

Une autre façon consiste à écrire une fonction UDF personnalisée qui renvoie la distance haversine de 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;

récupérer tous les enregistrements à 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 à une distance 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;
}

3

Une approximation rapide, simple et précise (pour les petites distances) peut être effectuée avec une projection sphérique . Au moins dans mon algorithme de routage, j'obtiens un boost 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 la précision 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);


3

Lisez la recherche de distance géographique avec MySQL , une solution basée sur la mise en œuvre de la formule Haversine sur MySQL. Il s'agit d'une description complète de la solution avec théorie, implémentation et optimisation des performances. Bien que la partie d'optimisation spatiale ne fonctionne pas correctement dans mon cas.

J'ai remarqué deux erreurs:

  1. l'utilisation de absdans l'instruction select à la p8. J'ai juste omis abset cela a fonctionné.

  2. la fonction de distance de recherche spatiale sur p27 ne se convertit pas en radians ou multiplie la longitude par cos(latitude), sauf si ses données spatiales sont chargées avec cela en considération (ne peut pas le dire à partir du contexte de l'article), mais son exemple sur p26 indique que ses données spatiales ne POINTsont pas chargées avec radians ou degrés.


0
$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";

0

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-nearest-loc/

REMARQUE: LEASTest utilisé pour éviter les valeurs nulles comme commentaire suggéré sur https://stackoverflow.com/a/24372831/5155484

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.