web-dev-qa-db-fra.com

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

J'ai actuellement un peu moins d'un million d'emplacements dans une base de données mysql avec toutes les 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 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 indiquée est en Miles . Si vous avez besoin de Kilomètres , utilisez 6371 au lieu de 3959.

210
Ryan Detzel
  • Créez vos points en utilisant les valeurs Point de types de données Geometry dans la table MyISAM. Depuis Mysql 5.7.5, les tables InnoDB prennent désormais en charge les index SPATIAL.

  • Créer un index SPATIAL 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, en MySQL 5.1 et ci-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 case (@lat +/- 10 km, @lon +/- 10km).

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 lui sur la plupart des lieux habités.

  • Appliquez un filtrage supplémentaire pour tout sélectionner à 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)

111
Quassnoi

Ce n'est pas une réponse spécifique à MySql, mais cela améliorera les performances de votre requête SQL.

Ce que vous faites effectivement, c'est calculer la distance par rapport à chaque point de la table, pour voir si elle se situe à 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éeLong - 10 unités, donnéeLat -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, elle peut utiliser des index sur les latitudes et les 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 capturer des points> 10u à partir du point donné dans les coins de la case), il vous reste donc besoin de 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 figurant dans la zone.

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

EDIT: Est-ce que ceci peut être mis dans une instruction SQL?

Je n'ai aucune idée de ce que mySql ou Php est capable de, 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 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 "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 vous aider davantage. 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.

93
Binary Worrier

Vérifiez cette présentation pour une bonne réponse . En gros, elle 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 calcul "dans la boîte" peut être très intéressant.

Recherche de géolocalisation avec MySQL

16
eillarra

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 compris dans le post, si vos champs de latitude et de longitude sont indexés , cela fonctionnera peut-être bien 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 un tableau appelé Lieux avec des champs latitude et longitude:

sélectionnez get_distance_in_miles_between_geo_locations (-34.017330, 22.809500, latitude, longitude) en tant que distance_de_poleure à partir de lieux;

tous accrochés de ce post

13
Brad Parks
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.

9
Sanni Poriya
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

7
Abhigyan

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
5
alriyami
   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;
4
user3113927

Le code complet avec les détails sur la façon d'installer MySQL est ici: https://github.com/lucasepe/lib_mysqludf_haversine

J'ai posté cette dernière année comme commentaire. Depuis la gentillesse @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 à partir 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;

chercher tous les records avec une distance inférieure à 40 kilomètres. Ou:

SELECT id, name FROM MY_PLACES WHERE haversine_distance(lat1, lng1, lat2, lng2, 'ft') < 25;

chercher tous les disques avec 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;
}
4
Luca Sepe

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

coordinates est un champ de type POINT et a SPATIAL index
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 coordinates pour sélectionner rapidement toutes les lignes qui se trouvent 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 d'un cercle.

Ceci est la visualisation de mon résultat:

 map

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 à l’intérieur des coins du rectangle (mais en dehors du cercle) ont été sélectionnées par MBRContains(), puis désélectionnées par la clause HAVING.

3
Māris Kiseļovs

Consultez la page Geo Distance Search avec MySQL , une solution basée sur l’implémentation de Haversine Formula à 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 optimisation spatiale ne fonctionne pas correctement dans mon cas. 

J'ai remarqué deux erreurs dans ceci: 

  1. l'utilisation de abs dans l'instruction select sur p8. J'ai simplement omis abs et cela a fonctionné.

  2. 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 à partir du contexte de l'article), mais son exemple sur p26 indique que ses données spatiales POINT ne sont pas chargé de radians ou de degrés.

3
Richard Sandoz

Voici une description très détaillée de Geo Distance Search avec MySQL, une solution basée sur l’implémentation 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-Search-with-MySQL

3
Konstantin Voronov

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);
3
Karussell

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

Pour utiliser cette fonction, appelez-la comme n'importe quelle autre fonction de MySQL. Par exemple, si vous avez une table city, vous pouvez trouver la distance entre chaque ville et toutes les autres 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`
3
Robert

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: LEAST est utilisé pour éviter les valeurs NULL. Un commentaire suggéré sur https://stackoverflow.com/a/24372831/5155484

0
William Desportes
$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
Neeraj Sharma