Qual fórmula você usa para a distância não importa muito. O que importa muito mais é o número de linhas que você precisa ler, processar e classificar. Na melhor das hipóteses, você pode usar um índice para uma condição na cláusula WHERE para limitar o número de linhas processadas. Você pode tentar categorizar seus locais - mas depende da natureza de seus dados, se isso funcionar bem. Você também precisa descobrir qual "categoria" usar. Uma solução mais geral seria usar um ÍNDICE ESPACIAL e o ST_Within() função.
Agora vamos fazer alguns testes..
No meu banco de dados (MySQL 5.7.18) tenho a seguinte tabela:
CREATE TABLE `cities` (
`cityId` MEDIUMINT(9) UNSIGNED NOT NULL AUTO_INCREMENT,
`country` CHAR(2) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`city` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`accentCity` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`region` CHAR(2) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
`population` INT(10) UNSIGNED NULL DEFAULT NULL,
`latitude` DECIMAL(10,7) NOT NULL,
`longitude` DECIMAL(10,7) NOT NULL,
`geoPoint` POINT NOT NULL,
PRIMARY KEY (`cityId`),
SPATIAL INDEX `geoPoint` (`geoPoint`)
) COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB
Os dados vêm do Banco de dados de cidades do mundo livre e contém 3173958 (3,1 M) linhas.
Observe que
geoPoint
é redundante e igual a POINT(longitude, latitude)
. Considere que o usuário está localizado em algum lugar em Londres
set @lon = 0.0;
set @lat = 51.5;
e você deseja encontrar o local mais próximo das
cities
tabela. Uma consulta "trivial" seria
select c.cityId, c.accentCity, st_distance_sphere(c.geoPoint, point(@lon, @lat)) as dist
from cities c
order by dist
limit 1
O resultado é
988204 Blackwall 1085.8212159861014
Tempo de execução:~ 4,970 seg
Se você usar a função menos complexa
ST_Distance()
, você obtém o mesmo resultado com um tempo de execução de ~ 4,580 seg - o que não é tanta diferença. Observe que você não precisa armazenar um ponto geográfico na tabela. Você também pode usar
(point(c.longitude, c.latitude)
em vez de c.geoPoint
. Para minha surpresa, é ainda mais rápido (~3,6 segundos para ST_Distance
e ~4,0 segundos para ST_Distance_Sphere
). Poderia ser ainda mais rápido se eu não tivesse um geoPoint
coluna em tudo. Mas isso ainda não importa muito, já que você não quer que o usuário espere, então registre por uma resposta, se puder fazer melhor. Agora vamos ver como podemos usar o ÍNDICE ESPACIAL com
ST_Within()
. Você precisa definir um polígono que conterá o local mais próximo. Uma maneira simples é usar ST_Buffer() que irá gerar um polígono com 32 pontos e é quase um círculo*.
set @point = point(@lon, @lat);
set @radius = 0.1;
set @polygon = ST_Buffer(@point, @radius);
select c.cityId, c.accentCity, st_distance_sphere(c.geoPoint, point(@lon, @lat)) as dist
from cities c
where st_within(c.geoPoint, @polygon)
order by dist
limit 1
O resultado é o mesmo. O tempo de execução é de ~ 0.000 seg (é o que meu cliente (HeidiSQL ) diz).
* Observe que o
@radius
é notado em graus e, portanto, o polígono será mais parecido com uma elipse do que com um círculo. Mas nos meus testes sempre obtive o mesmo resultado da solução simples e lenta. Eu investigaria mais casos extremos, antes de usá-lo no meu código de produção. Agora você precisa encontrar o raio ideal para sua aplicação/dados. Se for muito pequeno - você pode não obter resultados ou perder o ponto mais próximo. Se for muito grande, talvez seja necessário processar muitas linhas.
Aqui alguns números para o caso de teste fornecido:
- @radius =0,001:sem resultado
- @radius =0,01:exatamente um local (com sorte) - Tempo de execução ~ 0,000 seg
- @radius =0,1:55 locais - Tempo de execução ~ 0,000 seg
- @radius =1.0:2.183 locais - Tempo de execução ~ 0,030 seg