Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Qual abordagem é mais rápida para obter todos os POIs do MySQL/MariaDB com PHP/Laravel


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