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
.
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)
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.
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.
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
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 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;
}
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
index6371
sert à calculer la distance en kilomètres56.946285
est la latitude du point central24.105078
est la longitude du point central10
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:
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
.
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:
l'utilisation de abs
dans l'instruction select sur p8. J'ai simplement omis abs
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 à 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.
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
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);
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`
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
$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";