Encontre a latitude / longitude mais próxima com uma consulta SQL

173

Tenho latitude e longitude e quero extrair o registro do banco de dados, que tem a latitude e a longitude mais próximas pela distância, se essa distância for maior que a especificada, não o recupere.

Estrutura da tabela:

id
latitude
longitude
place name
city
country
state
zip
sealevel
Basit
fonte
1
Isso é uma espécie de duplicata da pergunta de pesquisa por proximidade .
Darius Bacon
1
Há um conjunto de slides de Alexander Rubin na pesquisa geográfica (proximidade) com MySQL (link em PDF)
Martijn Pieters

Respostas:

209
SELECT latitude, longitude, SQRT(
    POW(69.1 * (latitude - [startlat]), 2) +
    POW(69.1 * ([startlng] - longitude) * COS(latitude / 57.3), 2)) AS distance
FROM TableName HAVING distance < 25 ORDER BY distance;

onde [starlat] e [startlng] é a posição em que para iniciar a medição da distância.

Kaletha
fonte
49
Apenas uma observação de desempenho, é melhor não executar o sqrt da variável distance, mas, ao invés disso, quadrar o valor do teste '25' ... depois executar o sqrt nos resultados que passaram se você precisar mostrar a distância
sradforth
9
Qual seria a mesma consulta para a distância em metros? (que atualmente está em milhas, certo?)
httpete 28/11
8
Que medida é essa 25?
Steffan Donal
16
Apenas para esclarecer aqui 69.1 é o fator de conversão de milhas para graus de latitude. 57.3 é aproximadamente 180 / pi, então isso é conversão de graus em radianos, para a função cosseno. 25 é o raio de pesquisa em milhas. Essa é a fórmula a ser usada ao usar graus decimais e milhas estatutárias.
John Vance
8
Além disso, não leva em conta a curvatura da terra. Isso não seria um problema para os raios de pesquisa curtos. Caso contrário, as respostas de Evan e Igor são mais completas.
precisa
63

Solução do Google:

Criando a tabela

Ao criar a tabela MySQL, você deseja prestar atenção especial aos atributos lat e lng. Com os recursos atuais de zoom do Google Maps, você só precisa de 6 dígitos de precisão após o decimal. Para manter no mínimo o espaço de armazenamento necessário para sua tabela, você pode especificar que os atributos lat e lng sejam de tamanho flutuante (10,6). Isso permitirá que os campos armazenem 6 dígitos após o decimal, mais até 4 dígitos antes do decimal, por exemplo, -123.456789 graus. Sua tabela também deve ter um atributo id para servir como chave primária.

CREATE TABLE `markers` (
  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
  `name` VARCHAR( 60 ) NOT NULL ,
  `address` VARCHAR( 80 ) NOT NULL ,
  `lat` FLOAT( 10, 6 ) NOT NULL ,
  `lng` FLOAT( 10, 6 ) NOT NULL
) ENGINE = MYISAM ;

Preenchendo a tabela

Depois de criar a tabela, é hora de preenchê-la com dados. Os dados de amostra fornecidos abaixo são para cerca de 180 pizzarias espalhadas pelos Estados Unidos. No phpMyAdmin, você pode usar a guia IMPORT para importar vários formatos de arquivo, incluindo CSV (valores separados por vírgula). O Microsoft Excel e o Google Spreadsheets exportam para o formato CSV, para que você possa transferir facilmente dados de planilhas para tabelas MySQL através da exportação / importação de arquivos CSV.

INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Frankie Johnnie & Luigo Too','939 W El Camino Real, Mountain View, CA','37.386339','-122.085823');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Amici\'s East Coast Pizzeria','790 Castro St, Mountain View, CA','37.38714','-122.083235');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Kapp\'s Pizza Bar & Grill','191 Castro St, Mountain View, CA','37.393885','-122.078916');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Round Table Pizza: Mountain View','570 N Shoreline Blvd, Mountain View, CA','37.402653','-122.079354');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Tony & Alba\'s Pizza & Pasta','619 Escuela Ave, Mountain View, CA','37.394011','-122.095528');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Oregano\'s Wood-Fired Pizza','4546 El Camino Real, Los Altos, CA','37.401724','-122.114646');

Localizando locais com MySQL

Para encontrar locais em sua tabela de marcadores que estejam a uma certa distância de raio de uma determinada latitude / longitude, você pode usar uma instrução SELECT com base na fórmula Haversine. A fórmula de Haversine é usada geralmente para calcular distâncias de grandes círculos entre dois pares de coordenadas em uma esfera. Uma explicação matemática detalhada é dada pela Wikipedia e uma boa discussão sobre a fórmula relacionada à programação está no site da Movable Type.

Aqui está a instrução SQL que encontrará os 20 locais mais próximos que estão dentro de um raio de 40 km até a coordenada 37, -122. Ele calcula a distância com base na latitude / longitude dessa linha e na latitude / longitude alvo e, em seguida, solicita apenas linhas onde o valor da distância é menor que 25, ordena toda a consulta por distância e limita a 20 resultados. Para pesquisar por quilômetros em vez de milhas, substitua 3959 por 6371.

SELECT 
id, 
(
   3959 *
   acos(cos(radians(37)) * 
   cos(radians(lat)) * 
   cos(radians(lng) - 
   radians(-122)) + 
   sin(radians(37)) * 
   sin(radians(lat )))
) AS distance 
FROM markers 
HAVING distance < 28 
ORDER BY distance LIMIT 0, 20;

Este é encontrar latitudes e longitudes a uma distância inferior a 45 quilômetros.

Outra é encontrá-los a uma distância entre 28 e 29 milhas:

SELECT 
id, 
(
   3959 *
   acos(cos(radians(37)) * 
   cos(radians(lat)) * 
   cos(radians(lng) - 
   radians(-122)) + 
   sin(radians(37)) * 
   sin(radians(lat )))
) AS distance 
FROM markers 
HAVING distance < 29 and distance > 28 
ORDER BY distance LIMIT 0, 20;

https://developers.google.com/maps/articles/phpsqlsearch_v3#creating-the-map

Sviatoslav Oleksiv
fonte
1
Deveria ser HAVING distance < 25como consultamos locais em um raio de 40 km?
Vitalii Elenhaupt
deve ser> 25, então ele irá procurar todos os registros
Vidur Punj
você tem certeza @vidurpunj sobre> 25?
Amranur Rahman
Tentei a consulta sql usando: distância <25, mas foi encontrado nenhum resultado .. como as distâncias marcadores amostra todos são acima de 25 ...
IbrahimShendy
37, -122 coordenada, é essa latitude e longitude para a posição de onde precisamos encontrar a distância?
Prasobh.Kollattu 9/08/19
28

Aqui está minha solução completa implementada em PHP.

Esta solução usa a fórmula Haversine, conforme apresentado em http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL .

Note-se que a fórmula de Haversine experimenta fraquezas ao redor dos pólos. Esta resposta mostra como implementar a fórmula de grande distância do círculo grande para contornar isso, no entanto, optei por usar o Haversine porque é bom o suficiente para meus propósitos.

Estou armazenando latitude como DECIMAL (10,8) e longitude como DECIMAL (11,8). Espero que isso ajude!

showClosest.php

<?PHP
/**
 * Use the Haversine Formula to display the 100 closest matches to $origLat, $origLon
 * Only search the MySQL table $tableName for matches within a 10 mile ($dist) radius.
 */
include("./assets/db/db.php"); // Include database connection function
$db = new database(); // Initiate a new MySQL connection
$tableName = "db.table";
$origLat = 42.1365;
$origLon = -71.7559;
$dist = 10; // This is the maximum distance (in miles) away from $origLat, $origLon in which to search
$query = "SELECT name, latitude, longitude, 3956 * 2 * 
          ASIN(SQRT( POWER(SIN(($origLat - latitude)*pi()/180/2),2)
          +COS($origLat*pi()/180 )*COS(latitude*pi()/180)
          *POWER(SIN(($origLon-longitude)*pi()/180/2),2))) 
          as distance FROM $tableName WHERE 
          longitude between ($origLon-$dist/cos(radians($origLat))*69) 
          and ($origLon+$dist/cos(radians($origLat))*69) 
          and latitude between ($origLat-($dist/69)) 
          and ($origLat+($dist/69)) 
          having distance < $dist ORDER BY distance limit 100"; 
$result = mysql_query($query) or die(mysql_error());
while($row = mysql_fetch_assoc($result)) {
    echo $row['name']." > ".$row['distance']."<BR>";
}
mysql_close($db);
?>

./assets/db/db.php

<?PHP
/**
 * Class to initiate a new MySQL connection based on $dbInfo settings found in dbSettings.php
 *
 * @example $db = new database(); // Initiate a new database connection
 * @example mysql_close($db); // close the connection
 */
class database{
    protected $databaseLink;
    function __construct(){
        include "dbSettings.php";
        $this->database = $dbInfo['host'];
        $this->mysql_user = $dbInfo['user'];
        $this->mysql_pass = $dbInfo['pass'];
        $this->openConnection();
        return $this->get_link();
    }
    function openConnection(){
    $this->databaseLink = mysql_connect($this->database, $this->mysql_user, $this->mysql_pass);
    }

    function get_link(){
    return $this->databaseLink;
    }
}
?>

./assets/db/dbSettings.php

<?php
$dbInfo = array(
    'host'      => "localhost",
    'user'      => "root",
    'pass'      => "password"
);
?>

Pode ser possível aumentar o desempenho usando um procedimento armazenado do MySQL, conforme sugerido pelo artigo "Pesquisa por distância geográfica com o MySQL" postado acima.

Eu tenho um banco de dados de ~ 17.000 lugares e o tempo de execução da consulta é de 0,054 segundos.

circuitos
fonte
Como posso obter a distância em km ou metros? Saudações!
chemitaxis
2
milha * 1,609344 = km
Sinan Dizdarević
2
AVISO. Excelente solução, mas possui um bug. Todos absdevem ser removidos. Não é necessário levar o valor abs ao converter de graus em radianos e, mesmo se você o fez, está fazendo isso em apenas uma das latitudes. Por favor, edite-o para corrigir o erro.
Chango
1
E para quem quiser isso em metros: Converter 3956 milhas em quilômetros: raio da Terra; Converter 69 milhas em quilômetros: o comprimento aproximado de 1 grau de latitude em km; E insira a distância em quilômetros.
Chango
1
E substituir 69com 111,044736(esqueceu que no comentário acima)
rkeet
24

Caso você seja preguiçoso como eu, aqui está uma solução combinada com esta e outras respostas no SO.

set @orig_lat=37.46; 
set @orig_long=-122.25; 
set @bounding_distance=1;

SELECT
*
,((ACOS(SIN(@orig_lat * PI() / 180) * SIN(`lat` * PI() / 180) + COS(@orig_lat * PI() / 180) * COS(`lat` * PI() / 180) * COS((@orig_long - `long`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) AS `distance` 
FROM `cities` 
WHERE
(
  `lat` BETWEEN (@orig_lat - @bounding_distance) AND (@orig_lat + @bounding_distance)
  AND `long` BETWEEN (@orig_long - @bounding_distance) AND (@orig_long + @bounding_distance)
)
ORDER BY `distance` ASC
limit 25;
Evan
fonte
1
o que exatamente bounding_distancerepresenta? esse valor limita os resultados a uma milha? Então, nesse caso, ele retornará resultados dentro de uma milha?
james
1
@ bounding_distance está em graus aqui e é usado para acelerar os cálculos limitando a região de pesquisa efetiva. Por exemplo, se você sabe que seu usuário está em uma determinada cidade e sabe que possui alguns pontos nessa cidade, é possível definir com segurança sua distância delimitadora em alguns graus.
Evan
1
Qual fórmula de distância geográfica está usando?
Bbodenmiller 19/08/2015
12

Facil ;)

SELECT * FROM `WAYPOINTS` W ORDER BY
ABS(ABS(W.`LATITUDE`-53.63) +
ABS(W.`LONGITUDE`-9.9)) ASC LIMIT 30;

Apenas substitua as coordenadas pelas necessárias. Os valores devem ser armazenados como o dobro. Este é um exemplo funcional do MySQL 5.x.

Felicidades

Nicholas
fonte
2
Não faço ideia por upvote, OP quer limitar e ordem por certa distância, não limitar em 30 e ordem pordx+dy
OKM
3
Isso fez por mim. Não é o que o OP queria, mas o que eu queria, então obrigado por responder! :)
Webmaster G
O ABS mais externo () é suficiente.
dzona
6

Tente isso, ele mostra os pontos mais próximos das coordenadas fornecidas (dentro de 50 km). Funciona perfeitamente:

SELECT m.name,
    m.lat, m.lon,
    p.distance_unit
             * DEGREES(ACOS(COS(RADIANS(p.latpoint))
             * COS(RADIANS(m.lat))
             * COS(RADIANS(p.longpoint) - RADIANS(m.lon))
             + SIN(RADIANS(p.latpoint))
             * SIN(RADIANS(m.lat)))) AS distance_in_km
FROM <table_name> AS m
JOIN (
      SELECT <userLat> AS latpoint, <userLon> AS longpoint,
             50.0 AS radius, 111.045 AS distance_unit
     ) AS p ON 1=1
WHERE m.lat
BETWEEN p.latpoint  - (p.radius / p.distance_unit)
    AND p.latpoint  + (p.radius / p.distance_unit)
    AND m.lon BETWEEN p.longpoint - (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
    AND p.longpoint + (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
ORDER BY distance_in_km

Apenas mude <table_name>. <userLat>e<userLon>

Você pode ler mais sobre esta solução aqui: http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

smartmouse
fonte
5

Você está procurando por coisas como a fórmula de Haversine . Veja aqui também.

Existem outros, mas este é o mais citado.

Se você está procurando algo ainda mais robusto, convém examinar os recursos GIS dos seus bancos de dados. Eles são capazes de algumas coisas legais, como dizer se um ponto (Cidade) aparece dentro de um determinado polígono (Região, País, Continente).

Koobz
fonte
É realmente o mais citado, mas muitos artigos se referem a hardware de computação antigo quando se trata de declarações sobre computação imprecisa usando outros métodos. Veja também movable-type.co.uk/scripts/latlong.html#cosine-law
Arjan
5

As respostas originais para a pergunta são boas, mas as versões mais recentes do mysql (MySQL 5.7.6 on) suportam consultas geográficas, então agora você pode usar a funcionalidade incorporada em vez de fazer consultas complexas.

Agora você pode fazer algo como:

select *, ST_Distance_Sphere( point ('input_longitude', 'input_latitude'), 
                              point(longitude, latitude)) * .000621371192 
          as `distance_in_miles` 
  from `TableName`
having `distance_in_miles` <= 'input_max_distance'
 order by `distance_in_miles` asc

Os resultados são retornados, metersportanto, se você desejar, em KMvez de milhas, use em .0001vez de.000621371192

Os documentos do MySQL estão aqui

Sherman
fonte
Se possível, por favor, adicione a versão mysql em resposta.
Parixit
ST_Distance_Spherenão existe na instalação do meu host ( mysql Ver 15.1 Distrib 10.2.23-MariaDB). Eu li em algum lugar para substituir, ST_Distancemas as distâncias estão muito longe.
ashleedawg 21/07/19
@ashleedawg - A partir da versão, acho que você está usando o MariaDB, que é um fork do mysql. A partir desta conversa parece que MariaDB não implementouST_Distance_Sphere
Sherman
4

Verifique este código com base no artigo Pesquisa por distância geográfica com MySQL :

Exemplo: encontre os 10 hotéis mais próximos da minha localização atual em um raio de 16 quilômetros:

#Please notice that (lat,lng) values mustn't be negatives to perform all calculations

set @my_lat=34.6087674878572; 
set @my_lng=58.3783670308302;
set @dist=10; #10 miles radius

SELECT dest.id, dest.lat, dest.lng,  3956 * 2 * ASIN(SQRT(POWER(SIN((@my_lat -abs(dest.lat)) * pi()/180 / 2),2) + COS(@my_lat * pi()/180 ) * COS(abs(dest.lat) *  pi()/180) * POWER(SIN((@my_lng - abs(dest.lng)) *  pi()/180 / 2), 2))
) as distance
FROM hotel as dest
having distance < @dist
ORDER BY distance limit 10;

#Also notice that distance are expressed in terms of radius.
JuanManuelFigueroa
fonte
3
simpledb.execSQL("CREATE TABLE IF NOT EXISTS " + tablename + "(id INTEGER PRIMARY KEY   AUTOINCREMENT,lat double,lng double,address varchar)");
            simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2891001','70.780154','craftbox');");
            simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2901396','70.7782428','kotecha');");//22.2904718 //70.7783906
            simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2863155','70.772108','kkv Hall');");
            simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.275993','70.778076','nana mava');");
            simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2667148','70.7609386','Govani boys hostal');");


    double curentlat=22.2667258;  //22.2677258
    double curentlong=70.76096826;//70.76096826

    double curentlat1=curentlat+0.0010000;
    double curentlat2=curentlat-0.0010000;

    double curentlong1=curentlong+0.0010000;
    double curentlong2=curentlong-0.0010000;

    try{

        Cursor c=simpledb.rawQuery("select * from '"+tablename+"' where (lat BETWEEN '"+curentlat2+"' and '"+curentlat1+"') or (lng BETWEEN         '"+curentlong2+"' and '"+curentlong1+"')",null);

        Log.d("SQL ", c.toString());
        if(c.getCount()>0)
        {
            while (c.moveToNext())
            {
                double d=c.getDouble(1);
                double d1=c.getDouble(2);

            }
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
Hardip
fonte
2

Parece que você deseja fazer uma pesquisa de vizinho mais próximo com alguma distância. O SQL não suporta nada parecido, pelo que sei, e você precisaria usar uma estrutura de dados alternativa, como uma árvore R ou kd .

Chris de Vries
fonte
2

Encontre usuários mais próximos do meu:

Distância em metros

Baseado na fórmula de Vincenty

Eu tenho tabela de usuário:

+----+-----------------------+---------+--------------+---------------+
| id | email                 | name    | location_lat | location_long |
+----+-----------------------+---------+--------------+---------------+
| 13 | xxxxxx@xxxxxxxxxx.com | Isaac   | 17.2675625   | -97.6802361   |
| 14 | xxxx@xxxxxxx.com.mx   | Monse   | 19.392702    | -99.172596    |
+----+-----------------------+---------+--------------+---------------+

sql:

-- my location:  lat   19.391124   -99.165660
SELECT 
(ATAN(
    SQRT(
        POW(COS(RADIANS(users.location_lat)) * SIN(RADIANS(users.location_long) - RADIANS(-99.165660)), 2) +
        POW(COS(RADIANS(19.391124)) * SIN(RADIANS(users.location_lat)) - 
       SIN(RADIANS(19.391124)) * cos(RADIANS(users.location_lat)) * cos(RADIANS(users.location_long) - RADIANS(-99.165660)), 2)
    )
    ,
    SIN(RADIANS(19.391124)) * 
    SIN(RADIANS(users.location_lat)) + 
    COS(RADIANS(19.391124)) * 
    COS(RADIANS(users.location_lat)) * 
    COS(RADIANS(users.location_long) - RADIANS(-99.165660))
 ) * 6371000) as distance,
users.id
FROM users
ORDER BY distance ASC

raio da terra: 6371000 (em metros)

Isaac Limón
fonte
1

MS SQL Edition aqui:

        DECLARE @SLAT AS FLOAT
        DECLARE @SLON AS FLOAT

        SET @SLAT = 38.150785
        SET @SLON = 27.360249

        SELECT TOP 10 [LATITUDE], [LONGITUDE], SQRT(
            POWER(69.1 * ([LATITUDE] - @SLAT), 2) +
            POWER(69.1 * (@SLON - [LONGITUDE]) * COS([LATITUDE] / 57.3), 2)) AS distance
        FROM [TABLE] ORDER BY 3
B.Tekkan
fonte
0

Parece que você deve usar PostGIS, SpatialLite, SQLServer2008 ou Oracle Spatial. Todos eles podem responder a essa pergunta para você com SQL espacial.

TheSteve0
fonte
7
Parece que você NÃO deve sugerir que as pessoas mudem toda a sua plataforma de banco de dados e façam com que resultados irrelevantes apareçam na minha pesquisa no google quando pesquiso explicitamente pelo "Oracle" ...
Lutei com um urso uma vez.
0

Em casos extremos, essa abordagem falha, mas, para o desempenho, pulei a trigonometria e simplesmente calculei a diagonal ao quadrado.

user1032402
fonte
-13

Esse problema não é muito difícil, mas fica mais complicado se você precisar otimizá-lo.

O que quero dizer é que você tem 100 locais no seu banco de dados ou 100 milhões? Isso faz uma grande diferença.

Se o número de locais for pequeno, tire-os do SQL e entre no código apenas executando ->

Select * from Location

Depois de inseri-los no código, calcule a distância entre cada lat / lon e o original com a fórmula Haversine e classifique-a.

chamiltongt
fonte