Który wzór na odległość nie ma większego znaczenia. Dużo bardziej liczy się liczba wierszy, które musisz przeczytać, przetworzyć i posortować. W najlepszym przypadku możesz użyć indeksu dla warunku w klauzuli WHERE, aby ograniczyć liczbę przetwarzanych wierszy. Możesz spróbować skategoryzować swoje lokalizacje - ale to zależy od charakteru Twoich danych, czy to ma działać dobrze. Musisz również dowiedzieć się, której „kategorii” użyć. Bardziej ogólnym rozwiązaniem byłoby użycie INDEKSU PRZESTRZENNEGO i ST_Within() funkcja.
Przeprowadźmy teraz kilka testów.
W mojej bazie danych (MySQL 5.7.18) mam następującą tabelę:
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
Dane pochodzą z Free World Cities Database i zawiera 3173958 (3,1 mln) wierszy.
Zwróć uwagę, że geoPoint
jest nadmiarowy i równy POINT(longitude, latitude)
.
Uważam, że użytkownik znajduje się gdzieś w Londynie
set @lon = 0.0;
set @lat = 51.5;
i chcesz znaleźć najbliższą lokalizację z cities
tabela.
„Trywialne” zapytanie to
select c.cityId, c.accentCity, st_distance_sphere(c.geoPoint, point(@lon, @lat)) as dist
from cities c
order by dist
limit 1
Wynik to
988204 Blackwall 1085.8212159861014
Czas wykonania:~ 4,970 s
Jeśli używasz mniej złożonej funkcji ST_Distance()
, otrzymujesz ten sam wynik z czasem wykonania ~ 4,580 s - co nie jest tak dużą różnicą.
Pamiętaj, że nie musisz przechowywać punktu geograficznego w tabeli. Możesz równie dobrze użyć (point(c.longitude, c.latitude)
zamiast c.geoPoint
. Ku mojemu zdziwieniu jest jeszcze szybszy (~3,6 s dla ST_Distance
i ~4.0 s dla ST_Distance_Sphere
). Mogłoby być jeszcze szybciej, gdybym nie miał geoPoint
w ogóle kolumna. Ale to nadal nie ma większego znaczenia, ponieważ nie chcesz, aby użytkownik czekał, więc zaloguj się na odpowiedź, jeśli możesz zrobić to lepiej.
Zobaczmy teraz, jak możemy użyć INDEKSU PRZESTRZENNEGO z ST_Within()
.
Musisz zdefiniować wielokąt który będzie zawierał najbliższą lokalizację. Prostym sposobem jest użycie ST_Buffer() który wygeneruje wielokąt z 32 punktami i jest prawie okręgiem*.
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
Wynik jest taki sam. Czas wykonania wynosi ~0.000 s (to właśnie mój klient (HeidiSQL) ) mówi).
* Zwróć uwagę, że @radius
jest zapisany w stopniach, a zatem wielokąt będzie bardziej przypominał elipsę niż okrąg. Ale w moich testach zawsze uzyskiwałem ten sam wynik, co w przypadku prostego i wolnego rozwiązania. Chciałbym jednak zbadać więcej przypadków brzegowych, zanim użyję go w moim kodzie produkcyjnym.
Teraz musisz znaleźć optymalny promień dla swojej aplikacji/danych. Jeśli jest za mały - możesz nie uzyskać wyników lub przegapić najbliższy punkt. Jeśli jest za duży — może być konieczne przetworzenie zbyt wielu wierszy.
Oto kilka liczb dla danego przypadku testowego:
- @promień =0,001:Brak wyniku
- @radius =0,01:dokładnie jedna lokalizacja (trochę szczęścia) - czas wykonania ~ 0,000 s
- @radius =0,1:55 lokalizacji – czas wykonania ~ 0,000 s
- @radius =1.0:2183 lokalizacje – czas wykonania ~ 0,030 s