Il modo più veloce per trovare la distanza tra due punti lat / long


227

Al momento ho poco meno di un milione di posizioni in un database mysql tutte con informazioni di longitudine e latitudine.

Sto cercando di trovare la distanza tra un punto e molti altri punti tramite una query. Non è così veloce come voglio che sia soprattutto con oltre 100 colpi al secondo.

C'è una query più veloce o forse un sistema più veloce diverso da mysql per questo? Sto usando questa query:

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;

Nota: la distanza fornita è in miglia . Se hai bisogno di chilometri , usa 6371invece di 3959.


31
La formula che dai sembra avere molti elementi costanti. È possibile pre-calcolare i dati e archiviare anche questi valori nel proprio DB? Ad esempio 3959 * acos (cos (radians (42.290763)) è una costante ma contiene 4 calcoli principali al suo posto. Invece potresti semplicemente memorizzare 6696.7837?
Peter M,

1
O almeno pre-calcolare costanti al di fuori della query? Ciò ridurrà il lavoro che deve essere fatto.
Peter M,

2
@Peter M Sembra probabile che qualsiasi database SQL decente si ottimizzerebbe in modo da essere calcolato una sola volta.
mhenry1384,

25
Per quelli che si chiedono, 42.290763 è la latitudine e -71.35368 è la longitudine del punto da cui calcolare le distanze.
user276648

14
Solo per informazioni, la distanza confermata da questa formula è in miglia, non in chilometri. Sostituisci 3959 a 6371 per ottenere risultati in chilometri
Sahil

Risposte:


115
  • Crea i tuoi punti usando i Pointvalori dei Geometrytipi di dati nella MyISAMtabella. A partire da Mysql 5.7.5, le InnoDBtabelle ora supportano anche gli SPATIALindici.

  • Crea un SPATIALindice su questi punti

  • Utilizzare MBRContains()per trovare i valori:

    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)

o, in MySQL 5.1e sopra:

    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
                    )

Questo selezionerà tutti i punti approssimativamente all'interno del riquadro (@lat +/- 10 km, @lon +/- 10km).

In realtà non si tratta di una scatola, ma di un rettangolo sferico: segmento della sfera associato a latitudine e longitudine. Questo può differire da un semplice rettangolo sulla Terra di Franz Joseph , ma abbastanza vicino ad esso nella maggior parte dei luoghi abitati.

  • Applica un filtro aggiuntivo per selezionare tutto all'interno del cerchio (non il quadrato)

  • Eventualmente applicare un filtro fine aggiuntivo per tenere conto della grande distanza del cerchio (per grandi distanze)


15
@Quassnoi: un paio di correzioni: probabilmente vorrai cambiare l'ordine delle coordinate in lat, long. Inoltre, le distanze longitudinali sono proporzionali al coseno della latitudine , non alla longitudine. E vorrai cambiarlo da moltiplicazione a divisione, quindi la tua prima coordinata verrebbe corretta come @lon - 10 / ( 111.1 / cos(@lat))(ed essere la seconda nella coppia una volta che tutto fosse corretto.
M. Dave Auayan,

8
ATTENZIONE : il corpo della risposta NON è stato modificato in accordo con il commento molto valido fatto da @M. Dave Auayan. Ulteriori note: questo metodo diventa a forma di pera se il cerchio di interesse (a) include un polo o (b) è intersecato dal meridiano di +/- 180 gradi di longitudine. Anche l'utilizzo cos(lon)è accurato solo per le piccole distanze. Vedi janmatuschek.de/LatitudeLongitudeBoundingCoordinates
John Machin

3
Esiste un modo per ottenere una visione di ciò che rappresentano le costanti (10, 111.11, @lat, @lon, mypoint)? Presumo che il 10 sia per la distanza di chilometri, @lat e @lon rappresentano il reticolo e la longitudine forniti, ma cosa rappresentano 111.11 e mypoint nell'esempio?
ashays,

4
@ashays: ci sono circa 111.(1)km in un grado di latitudine. mypointè il campo nella tabella in cui sono memorizzate le coordinate.
Quassnoi,

1
Un'altra correzione di errori - ti manca una chiusura) sulla penultima riga
ina

100

Non è una risposta specifica per MySql, ma migliorerà le prestazioni della tua dichiarazione sql.

Quello che stai effettivamente facendo è calcolare la distanza da ogni punto della tabella, per vedere se è entro 10 unità di un dato punto.

Quello che puoi fare prima di eseguire questo sql è creare quattro punti che disegnano una scatola di 20 unità su un lato, con il tuo punto al centro. (x1, y1). . . (x4, y4), dove (x1, y1) è (dato lungo + 10 unità, dato Lat + 10 unità). . . (datoLungo - 10 unità, datoLat -10 unità). In realtà, hai solo bisogno di due punti, in alto a sinistra e in basso a destra chiamali (X1, Y1) e (X2, Y2)

Ora la tua istruzione SQL usa questi punti per escludere righe che sono sicuramente più di 10u dal tuo punto dato, può usare gli indici su latitudini e longitudini, quindi gli ordini di grandezza saranno più veloci di quello che hai attualmente.

per esempio

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

L'approccio box può restituire falsi positivi (è possibile raccogliere punti negli angoli del box che sono> 10u da un determinato punto), quindi è comunque necessario calcolare la distanza di ciascun punto. Tuttavia, questo sarà di nuovo molto più veloce perché hai drasticamente limitato il numero di punti da testare ai punti all'interno del riquadro.

Chiamo questa tecnica "Pensare dentro la scatola" :)

EDIT: questo può essere inserito in un'istruzione SQL?

Non ho idea di cosa sia capace mySql o Php, scusa. Non so dove sia il posto migliore per costruire i quattro punti o come possano essere passati a una query mySql in Php. Tuttavia, una volta che hai i quattro punti, non c'è niente che ti impedisce di combinare la tua istruzione SQL con la mia.

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;

So che con MS SQL posso creare un'istruzione SQL che dichiara quattro float (X1, Y1, X2, Y2) e li calcola prima dell'istruzione select "principale", come ho detto, non ho idea se questo può essere fatto con MySql. Tuttavia, sarei ancora propenso a costruire i quattro punti in C # e passarli come parametri alla query SQL.

Mi dispiace non poter essere più di aiuto, se qualcuno può rispondere a specifiche porzioni di MySQL e Php di questo, sentiti libero di modificare questa risposta per farlo.


4
Puoi trovare una procedura mysql per questo approccio in questa presentazione: scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
Lucia

37
Per cercare per chilometri anziché per miglia, sostituisci 3959 con 6371.
ErichBSchulz,

4
+1, ottima opzione; aggiungendo la casella ho ridotto la mia richiesta da 4s a 0.03s avg.
Jvenema,

1
Anche se sembra così logico, ti riservi un premio per questa soluzione! Su un database di record da 2 milioni la query è passata da 16 secondi a 0,06 secondi. Nota: è ancora più veloce (per tabelle di grandi dimensioni) se si elimina il calcolo della distanza dalla query e si esegue il calcolo della distanza nel codice del programma!
NLAnaconda,

2
@Binary Worrier: Quindi X1, X2 e Y1, Y2 saranno Longitudine Min e Max e Latitudine Min e Max come nell'esempio fornito qui: blog.fedecarg.com/2009/02/08/…, si prega di avvisare.
Prabhat,

14

La seguente funzione MySQL è stata pubblicata su questo post del blog . Non l'ho testato molto, ma da quello che ho raccolto dal post, se i tuoi campi di latitudine e longitudine sono indicizzati , questo potrebbe funzionare bene per te:

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 ;

Esempio di utilizzo:

Supponendo una tabella chiamata placescon campi latitudee longitude:

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

Ho provato questo e funziona perfettamente, ma in qualche modo non mi consente di inserire un'istruzione WHERE basata su distance_from_input. Qualche idea sul perché no?
Chris Visser,

potresti farlo come sottoselezione: seleziona * da (...) come t dove distance_from_input> 5;
Brad Parks,

2
oppure vai dritto con: seleziona * dai luoghi in cui get_distance_in_miles_between_geo_locations (-34.017330, 22.809500, latitudine, longitudine)> 5000;
Brad Parks,

2
Metri di ritorno: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

Avevo bisogno di risolvere un problema simile (filtrando le righe per distanza dal singolo punto) e combinando la domanda originale con risposte e commenti, ho trovato una soluzione che funziona perfettamente per me sia su MySQL 5.6 che 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è un campo con tipo POINTe ha un SPATIALindice
6371è per il calcolo della distanza in chilometri
56.946285è la latitudine per il punto centrale
24.105078è la longitudine per il punto centrale
15è la distanza massima in chilometri

Nei miei test, MySQL utilizza l'indice SPATIAL sul coordinatescampo per selezionare rapidamente tutte le righe all'interno del rettangolo, quindi calcola la distanza effettiva per tutti i luoghi filtrati per escludere i punti dagli angoli dei rettangoli e lasciare solo i punti all'interno del cerchio.

Questa è la visualizzazione del mio risultato:

carta geografica

Le stelle grigie visualizzano tutti i punti sulla mappa, le stelle gialle sono quelle restituite dalla query MySQL. Le stelle grigie all'interno degli angoli del rettangolo (ma il cerchio esterno) sono state selezionate MBRContains()e quindi deselezionate dalla HAVINGclausola.


Non posso votarlo abbastanza. Cercando attraverso una tabella con circa 5 milioni di record e un indice spaziale con questo metodo il tempo di ricerca è di 0,005 secondi su un vecchio processore A8. So che 6371 può essere sostituito con 3959 per ottenere risultati in miglia, ma i valori di 111.133 e 111.320 devono essere regolati o sono universalmente costanti?
Wranorn,

Ottima soluzione
SeaBiscuit il

Come creare Point is POINT (lat, lng) o POINT (lng, lat)
user606669

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

La funzione X () e Y () dovrebbe essere ST_Y e ST_X al giorno d'oggi.
Andreas,

11

se si utilizza MySQL 5.7. *, è possibile utilizzare st_distance_sphere (POINT, POINT) .

Select st_distance_sphere(POINT(-2.997065, 53.404146 ), POINT(58.615349, 23.56676 ))/1000  as distcance

1
questa è un'alternativa molto buona e di facile lettura. tieni presente che l'ordine dei parametri su POINT () è (lng, lat) altrimenti potresti finire con "chiudi" ma risultati comunque molto diversi dagli altri metodi qui. vedi: 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

Questa è la query di calcolo della distanza tra i punti in MySQL, l'ho usato in un lungo database, funziona perfettamente! Nota: apportare le modifiche (nome del database, nome della tabella, colonna ecc.) In base alle proprie esigenze.


Cosa rappresenta il valore 1.1515? Ho già visto una formula simile prima, ma ha usato 1,75 invece di 1,1515.
Prova Harder il

1
In risposta alla mia domanda, penso che la risposta potrebbe trovarsi qui 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);

fonte


11
Per favore, cita le tue fonti. Questo è da: blog.fedecarg.com/2009/02/08/…
redburn

Cosa sono 69 in questo caso? Come fare nel caso in cui abbiamo il raggio terrestre?
CodeRunner,

2
Chilometri in 1 Latittude è 111 KM. Il miglio in 1 Latittude è di 69 miglia. e 69 Miglia = 111 KM. Ecco perché abbiamo utilizzato i parametri nelle conversioni.
CodeRunner,

Lo cercavo da sempre. Non sapevo che potesse essere così semplice. Grazie mille.
Vikas,

Non sarebbe errato in quanto lng_min / lng_max avrebbe bisogno di usare lat_min e lat_max nella matematica del raggio?
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

Una funzione MySQL che restituisce il numero di metri tra le due coordinate:

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

Per restituire il valore in un formato diverso, sostituire il 6371000nella funzione con il raggio della Terra nella scelta dell'unità. Ad esempio, i chilometri sarebbero 6371e le miglia sarebbero 3959.

Per usare la funzione, chiamala come faresti con qualsiasi altra funzione in MySQL. Ad esempio, se avessi un tavolo city, potresti trovare la distanza tra ogni città e ogni altra città:

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

Il codice completo con i dettagli su come installare come plugin MySQL sono qui: https://github.com/lucasepe/lib_mysqludf_haversine

Ho pubblicato lo scorso anno come commento. Dal momento che @TylerCollier mi ha gentilmente consigliato di postare come risposta, eccolo qui.

Un altro modo è quello di scrivere una funzione UDF personalizzata che restituisca la distanza haversine da due punti. Questa funzione può contenere input:

lat1 (real), lng1 (real), lat2 (real), lng2 (real), type (string - optinal - 'km', 'ft', 'mi')

Quindi possiamo scrivere qualcosa del genere:

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

per recuperare tutti i record con una distanza inferiore a 40 chilometri. O:

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

per recuperare tutti i record con una distanza inferiore a 25 piedi.

La funzione principale è:

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

Un'approssimazione veloce, semplice e accurata (per distanze più piccole) può essere fatta con una proiezione sferica . Almeno nel mio algoritmo di routing ottengo un aumento del 20% rispetto al calcolo corretto. Nel codice Java sembra:

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

Non sono sicuro di MySQL (scusate!).

Assicurati di conoscere la limitazione (il terzo parametro di assertEquals indica l'accuratezza in chilometri):

    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

Ecco una descrizione molto dettagliata di Geo Distance Search con MySQL una soluzione basata sull'implementazione di Haversine Formula a mysql. La descrizione completa della soluzione con teoria, implementazione e ulteriore ottimizzazione delle prestazioni. Sebbene la parte di ottimizzazione spaziale non abbia funzionato correttamente nel mio caso. http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL


3

Leggi Geo Distance Search con MySQL , una soluzione basata sull'implementazione di Haversine Formula su MySQL. Questa è una descrizione completa della soluzione con teoria, implementazione e ulteriore ottimizzazione delle prestazioni. Sebbene la parte di ottimizzazione spaziale non abbia funzionato correttamente nel mio caso.

Ho notato due errori in questo:

  1. l'uso di absnell'istruzione select su p8. Ho appena omesso abse ha funzionato.

  2. la funzione di distanza di ricerca spaziale su p27 non converte in radianti o moltiplica la longitudine per cos(latitude), a meno che i suoi dati spaziali non siano caricati con questo in considerazione (non si può dire dal contesto dell'articolo), ma il suo esempio su p26 indica che i suoi dati spaziali POINTnon sono caricati con radianti o gradi.


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

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

Vedi: https://andrew.hedges.name/experiments/haversine/

Vedi: https://stackoverflow.com/a/24372831/5155484

Vedi: http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

NOTA: LEASTviene utilizzato per evitare valori nulli come suggerito da un commento su https://stackoverflow.com/a/24372831/5155484

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.