Найшвидший спосіб знайти відстань між двома точками Lat / Long


227

Наразі в базі даних mysql є трохи менше мільйона локацій, все з інформацією про довготу та широту.

Я намагаюся знайти відстань між однією точкою та багатьма іншими точками за допомогою запиту. Це не так швидко, як я хочу, щоб це було особливо зі 100+ ударами в секунду.

Чи є для цього більш швидкий запит або, можливо, більш швидка система, крім mysql? Я використовую цей запит:

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;

Примітка. Забезпечена відстань в милях . Якщо вам потрібні кілометри , використовуйте 6371замість цього 3959.


31
Формула, яку ви надаєте, здається, має багато постійних елементів. Чи можливо попередньо обчислити дані та зберегти ці значення також у вашій БД? Наприклад, 3959 * acos (cos (радіани (42.290763)) - це константа, але в ній є 4 основні обчислення. Натомість ви могли б просто зберігати 6696.7837?
Peter M

1
Або принаймні попередньо обчислити константи поза запитом? Це скоротить роботу, яку потрібно виконати.
Пітер М

2
@Peter M Мабуть, будь-яка пристойна база даних SQL оптимізувала б так, що вона була обчислена лише один раз.
mhenry1384

25
Для тих, хто цікавиться, 42.290763 - це широта, а -71.35368 - довгота точки, з якої потрібно обчислити відстані.
користувач276648

14
Для отримання інформації: Відстань, обчислена за цією формулою, знаходиться в милях, а не в кілометрах. Замініть 3959 на 6371, щоб отримати результати в кілометрах
Сахіль

Відповіді:


115
  • Створіть свої бали, використовуючи Pointзначення Geometryтипів даних у MyISAMтаблиці. Станом на Mysql 5.7.5, InnoDBтаблиці також підтримують SPATIALіндекси.

  • Створіть SPATIALіндекс за цими точками

  • Використовуйте MBRContains()для пошуку значень:

    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)

, або, MySQL 5.1вище та вище:

    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
                    )

Це дозволить вибрати всі точки приблизно в полі (@lat +/- 10 km, @lon +/- 10km).

Це насправді не коробка, а сферичний прямокутник: широта та довгота, пов'язані сегментом сфери. Це може відрізнятися від простого прямокутника на землі Франца-Йосифа , але досить близько до нього в більшості населених місць.

  • Застосуйте додаткову фільтрацію, щоб вибрати все всередині кола (а не квадрат)

  • Можливо застосувати додаткову фільтр тонкої фіксації для врахування великої відстані кола (для великих відстаней)


15
@Quassnoi: Пара виправлень: Ви, мабуть, захочете переключити порядок координат на лат, довгий. Також поздовжні відстані пропорційні косинусу широти , а не довготи. І ви захочете змінити його з множення на ділення, щоб ваша перша координата була виправлена ​​як @lon - 10 / ( 111.1 / cos(@lat))(і бути другою в парі, як тільки все було правильно.
М. Дейв Ауаян

8
ПОПЕРЕДЖЕННЯ : Основа відповіді НЕ була відредагована відповідно до дуже вагомого коментаря, зробленого @M. Дейв Ауаян. Подальші зауваження: Цей спосіб перетворюється на певну форму, якщо коло, що цікавить (а), включає полюс або (b), перетинається меридіаном +/- 180 градусів довготи. Також використання cos(lon)точне лише для невеликих відстаней. Дивіться janmatuschek.de/LatitudeLongitudeBoundingCoordinate
Джон

3
Чи є якийсь спосіб, щоб ми могли отримати деяке уявлення про те, що являють собою константи (10, 111.11, @lat, @lon, mypoint)? Я припускаю, що 10 знаходиться на відстані кілометрів, @lat і @lon являють собою передбачені ґрати та довготу, але що в прикладі відображають 111.11 та точки?
ashays

4
@ashays: є приблизно 111.(1)км за ступенем широти. mypoint- поле в таблиці, в якому зберігаються координати.
Quassnoi

1
Ще одне виправлення помилок - вам не вистачає закриття) у другому до останнього рядка
іна

100

Не відповідь MySql, але це покращить продуктивність вашої операції sql.

Те, що ви ефективно робите, - це обчислити відстань до кожної точки таблиці, щоб побачити, чи знаходиться вона в межах 10 одиниць даної точки.

Що ви можете зробити перед тим, як запустити цей sql, це створити чотири точки, які намалюють коробку з 20 одиницями збоку, з точкою в центрі, тобто. (x1, y1). . . (x4, y4), де (x1, y1) є (дається тривалість + 10 одиниць, задана лат + 10 одиниць). . . (Дана Довга - 10 Одиниць, ДанаЛітка -10 одиниць). Насправді, вам потрібно лише дві точки, зверху ліворуч і праворуч зверніть їх (X1, Y1) та (X2, Y2)

Тепер ваш оператор SQL використовує ці точки, щоб виключити рядки, які, безумовно, перевищують 10u від вашої заданої точки, він може використовувати індекси широт і довжин, тому буде на порядок швидше, ніж у вас зараз.

напр

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

Підхід коробки може повернути помилкові позитиви (ви можете забрати точки в кутах поля, які> 10u від даної точки), тому вам все одно потрібно обчислити відстань кожної точки. Однак це знову буде набагато швидше, оскільки ви різко обмежили кількість балів для тестування до балів у полі.

Я називаю цю техніку "Мислення всередині коробки" :)

EDIT: Чи можна це помістити в один оператор SQL?

Я поняття не маю, на що здатний mySql або Php, вибачте. Я не знаю, де найкраще місце для побудови чотирьох точок, або як їх можна передати на запит mySql в Php. Однак, як тільки у вас є чотири точки, ніщо не зупинить вас, поєднуючи свій власний оператор SQL з моїм.

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;

Я знаю, що за допомогою MS SQL я можу створити оператор SQL, який оголошує чотири поплавці (X1, Y1, X2, Y2) і обчислює їх перед "головним" оператором select, як я вже сказав, я не маю уявлення, чи можна це зробити за допомогою MySql. Однак я все-таки схильний будувати чотири точки в C # і передавати їх у якості параметрів SQL-запиту.

Вибачте, я не можу більше допомогти, якщо хтось може відповісти на конкретні частини MySQL & Php, сміливо відредагуйте цю відповідь, щоб це зробити.


4
Ви можете знайти процедуру mysql для цього підходу в цій презентації: scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
Lucia

37
Щоб шукати кілометри замість миль, замініть 3959 на 6371.
ErichBSchulz

4
+1, чудовий варіант; Додавання поля зменшило мій запит із 4 до 0,03 с.
jvenema

1
Хоча це здається логічним, ви залишаєте нагороду за це рішення! У базі даних запису на 2 мільйони запит перейшов від 16 секунд до 0,06 секунди. Примітка: Це ще швидше (для великих таблиць), якщо ви вирізати обчислення відстані з запиту і зробити розрахунок відстані у своєму програмному коді!
NLAnaconda

2
@ Бінарне загроза: Отже, X1, X2 та Y1, Y2 будуть мінімальними по довжині та максимуму, а також по ширині Min та Max відповідно до наведеного тут прикладу: blog.fedecarg.com/2009/02/08/…, будь ласка, порадьте.
Прабхат

14

У цій публікації блогу була розміщена наступна функція MySQL . Я не тестував його багато, але з того, що я зібрав з посади, якщо ваші поля широти та довготи індексуються , це може спрацювати для вас:

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 ;

Використання зразка:

Припускаючи , що таблиця з ім'ям placesз полями latitudeі longitude:

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

Я спробував це, і він працює чудово, але якимось чином не дозволяє мені помістити заяву WHERE на основі distance_from_input. Будь-яка ідея, чому б і ні?
Кріс Віссер

ви можете зробити це як підбір: виберіть * з (...) як t, де відстань_від_введення> 5;
Бред Паркс

2
або просто рухайтеся прямо з: виберіть * з місць, де get_distance_in_miles_between_geo_locations (-34.017330, 22.809500, широта, довгота)> 5000;
Бред Паркс

2
повернення метрів: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
Мохаммед

13

Мені потрібно було вирішити подібну проблему (фільтруючи рядки по відстані від однієї точки) і, поєднавши оригінальне запитання з відповідями та коментарями, я придумав рішення, яке ідеально працює для мене як на MySQL 5.6, так і на 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поле з типом POINTі має SPATIALіндекс
6371для обчислення відстані в кілометрах
56.946285- широта для центральної точки
24.105078- довгота для центральної точки
15- максимальна відстань у кілометрах

У моїх тестах MySQL використовує SPATIAL індекс на coordinatesполі для швидкого вибору всіх рядків, що знаходяться у прямокутнику, а потім обчислює фактичну відстань для всіх відфільтрованих місць, щоб виключити місця з кутів прямокутника та залишити лише місця всередині кола.

Це візуалізація мого результату:

карта

Сірі зірки візуалізують усі точки на карті, жовті зірки - це ті, які повертаються запитом MySQL. Сірі зірки всередині кутів прямокутника (але поза колом) були вибрані MBRContains()і відмінені HAVINGпунктом за допомогою пункту.


Неможливо цього підтвердити. Шукаючи через таблицю з приблизно 5 мільйонами записів та просторовим індексом за допомогою цього методу, час пошуку на старому процесорі A8 становить 0,005 секунди. Я знаю, що 6371 можна замінити на 3959, щоб отримати результати в милях, але чи потрібно значення 111,133 та 111,320 коригувати чи вони універсально постійні?
Wranorn

Прекрасне рішення.
SeaBiscuit

Як створити точку - це POINT (лат, lng) або POINT (lng, lat)
user606669

2
@ user606669 Це POINT (lng, lat)
Māris Kiseļovs

Функції X () і Y () мають бути ST_Y і ST_X сьогодні.
Андреас

11

якщо ви використовуєте MySQL 5.7. *, ви можете використовувати st_distance_sphere (POINT, POINT) .

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

1
це дуже хороша та зручна для читання альтернатива. майте на увазі, порядковий параметр POINT () - це (lng, lat), інакше ви можете закінчити "закрити", але все ще дуже відрізняються від інших методів тут. дивіться: stackoverflow.com/questions/35939853/…
Енді П

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

Це запит обчислення відстані між точками в MySQL, я використовував його у довгій базі даних, він ідеально працює! Примітка: внесіть зміни (ім'я бази даних, назва таблиці, стовпчик тощо) відповідно до ваших вимог.


Що означає значення 1,1515? Раніше я бачив подібну формулу, але вона використовувала 1,75 замість 1,1515.
TryHarder

1
У відповідь на мій власний питання, я думаю , що відповідь може лежати тут 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);

джерело


11
Будь ласка, цитуйте свої джерела. Це з: blog.fedecarg.com/2009/02/08/…
redburn

Що в цьому випадку 69? Як це зробити у випадку, якщо у нас є радіус землі?
CodeRunner

2
Кілометр в 1 широті становить 111 КМ. Миля в 1 широті - 69 миль. і 69 миль = 111 КМ. Ось чому ми використовували параметри в конверсіях.
CodeRunner

Я шукав цього вічно. Не знав, що це може бути так просто. Дуже дякую.
Вікас

Це не буде неправильно, оскільки lng_min / lng_max потрібно буде використовувати lat_min та lat_max у математиці радіусів?
Бен

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

Функція MySQL, яка повертає кількість метрів між двома координатами:

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

Щоб повернути значення в іншому форматі, замініть 6371000функцію на радіус Землі у виборі одиниці. Наприклад, кілометри були б 6371і милі були б3959 .

Щоб використовувати цю функцію, просто зателефонуйте їй, як і будь-яка інша функція в MySQL. Наприклад, якщо у вас була таблиця city, ви можете знайти відстань між кожним містом та кожним іншим містом:

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

Повний код із деталями щодо встановлення як плагін MySQL тут: https://github.com/lucasepe/lib_mysqludf_haversine

Я опублікував це минулого року як коментар. Оскільки люб’язно @TylerCollier запропонував мені опублікувати як відповідь, ось це.

Інший спосіб - написати власну функцію UDF, яка повертає відстань haversine від двох точок. Ця функція може приймати дані:

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

Тож ми можемо написати щось подібне:

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

взяти всі рекорди з відстані менше 40 кілометрів. Або:

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

для отримання всіх записів на відстані менше 25 футів.

Основна функція:

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

Швидке, просте і точне (на менші відстані) наближення можна здійснити за допомогою сферичної проекції . Принаймні, в алгоритмі маршрутизації я отримую 20% -ве збільшення порівняно з правильним розрахунком. У коді Java це виглядає так:

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

Не впевнений у MySQL (вибачте!).

Будьте впевнені, що ви знаєте про обмеження (третій параметр assertEquals означає точність у кілометрах):

    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

Ось дуже детальний опис пошуку геоданих за допомогою MySQL, який базується на впровадженні формули Haversine в mysql. Повний опис рішення з теорією, реалізацією та подальшою оптимізацією продуктивності. Хоча частина просторової оптимізації в моєму випадку не працювала правильно. http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL


3

Прочитайте географічний пошук за допомогою MySQL геодані за , рішення, засноване на впровадженні формули Haversine в MySQL. Це повний опис рішення з теорією, реалізацією та подальшою оптимізацією продуктивності. Хоча частина просторової оптимізації в моєму випадку працювала неправильно.

Я помітив у цьому дві помилки:

  1. використання absв операторі select на p8. Я просто опустив, absі це спрацювало.

  2. функція просторового відстані пошуку на p27 не перетворюється в радіани і не помножує довготу на cos(latitude), якщо тільки його просторові дані не завантажені з цього приводу (не можна сказати з контексту статті), але його приклад на p26 вказує, що його просторові дані POINTне завантажуються радіани або градуси.


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

Використання 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;

Дивіться: https://andrew.hedges.name/experiment/haversine/

Дивіться: https://stackoverflow.com/a/24372831/5155484

Дивіться: http://www.plumislandmedia.net/mysql/haversine-mysql-nevable-loc/

ПРИМІТКА: LEASTвикористовується для уникнення нульових значень як коментаря, запропонованого на веб- сайті https://stackoverflow.com/a/24372831/5155484

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.