2011-01-19 2 views
3

Мне нужно написать запрос, который позволяет мне найти все местоположения в пределах диапазона (миль) из предоставленного местоположения.Расстояние между двумя координатами, как я могу упростить это и/или использовать другую технику?

таблица выглядит так:

id | name | lat | lng 

Так я делал исследование и обнаружили: this my sql presentation

Я испытал его на стол с около 100 строк и будет иметь много больше! - Должен быть масштабируемым.

Я пытался что-то более простое, как это первая:

//just some test data this would be required by user input  
set @orig_lat=55.857807; set @orig_lng=-4.242511; set @dist=10; 

SELECT *, 3956 * 2 * ASIN(
      SQRT(POWER(SIN((orig.lat - abs(dest.lat)) * pi()/180/2), 2) 
       + COS(orig.lat * pi()/180) * COS(abs(dest.lat) * pi()/180) 
       * POWER(SIN((orig.lng - dest.lng) * pi()/180/2), 2))) 
      AS distance 
    FROM locations dest, locations orig 
WHERE orig.id = '1' 
HAVING distance < 1 
ORDER BY distance; 

Это возвращенное строки в вокруг 50мс который очень хорошо! Однако это резко снизится по мере увеличения рядов.

EXPLAIN показывает, что используется только ключ PRIMARY.


Затем после прочтения статьи linked above. Я пытался что-то вроде этого:

// defining variables - this when made into a stored procedure will call 
// the values with a SELECT query. 
set @mylon = -4.242511; 
set @mylat = 55.857807; 
set @dist = 0.5; 

-- calculate lon and lat for the rectangle: 
set @lon1 = @[email protected]/abs(cos(radians(@mylat))*69); 
set @lon2 = @[email protected]/abs(cos(radians(@mylat))*69); 
set @lat1 = @mylat-(@dist/69); 
set @lat2 = @mylat+(@dist/69); 

-- run the query: 

SELECT *, 3956 * 2 * ASIN(
      SQRT(POWER(SIN((@mylat - abs(dest.lat)) * pi()/180/2) ,2) 
       + COS(@mylat * pi()/180) * COS(abs(dest.lat) * pi()/180) 
       * POWER(SIN((@mylon - dest.lng) * pi()/180/2), 2))) 
      AS distance 
    FROM locations dest 
WHERE dest.lng BETWEEN @lon1 AND @lon2 
    AND dest.lat BETWEEN @lat1 AND @lat2 
HAVING distance < @dist 
ORDER BY distance; 

Время этого запроса составляет около 240ms, это не так уж плохо, но медленнее, чем в прошлом. Но я могу себе представить, что при гораздо более высоком числе строк это будет работать быстрее. Однако EXPLAIN показывает возможные ключи как lat, lng или PRIMARY и используется PRIMARY.

Как я могу сделать это лучше ???

Я знаю, что могу хранить lat lng как POINT(); но я также не нашел слишком много документации по этому поводу, которая показывает, является ли она более быстрой или точной?

Любые другие идеи были бы с радостью приняты!

Большое спасибо!

-Stefan


UPDATE:

Как Джонатан Леффлера отметил, что я сделал несколько ошибок, которые я не заметил:

я только положить абс() на одном из значений lat. Я использовал поиск id в предложении WHERE во втором, когда не было необходимости. В первом запросе была чисто экспериментальная, вторая, скорее всего, поразила производство.

После этих изменений EXPLAIN показывает ключ теперь использует lng колонки и среднее время, чтобы ответить вокруг Теперь звука 180 мс, который является усовершенствованием.

+0

Стефан, я ищу, чтобы сделать что-то подобное .. можете ли вы опубликовать свою окончательную хранимую процедуру? Я никогда не писал хранимую процедуру раньше, первый вопрос, который приходит на ум, - ваш код выглядит так, как будто он имеет статические параметры .. как я могу передать myLat, myLon и расстояние до хранимой процедуры, и это расстояние «мили» « – erik

ответ

2

Любые другие идеи были бы с радостью приняты!

Если вам нужна скорость (и простота), вы захотите получить некоторую приличную геопространственную поддержку из своей базы данных. Это вводит геопространственные типы данных, геопространственные индексы и (многие) функции для обработки/построения/анализа геопространственных данных.

MySQL implements a part of the OpenGIS specifications хотя он/был (последний раз, когда я проверил его) был очень грубым по краям/преждевременно (не полезно для какой-либо реальной работы).

PostGis на PostgreSql бы это тривиально проста и читабельным:

(это находит все точки из TableB, которые ближе чем 1000 метров от точки а в TableA с идентификатором 123)

select 
    myvalue 
from 
    tablea, tableb 
where 
    st_dwithin(tablea.the_geom, tableb.the_geom, 1000) 
and 
    tablea.id = 123 
0

Некоторые мысли на повышение производительности. Это не упростит ситуацию с точки зрения ремонтопригодности (делает вещи более сложными), но это может помочь с масштабируемостью.

  1. Поскольку вы знаете радиус, вы можете добавить условия для кадрирования, который может позволить дб для оптимизации запроса, чтобы устранить некоторые строки без необходимости делать тригонометрические Calcs.

  2. Вы можете предварительно вычислить некоторые из значений триггера lat/lon сохраненных мест и сохранить их в таблице. Это приведет к смещению некоторых из стоимости исполнения при вставке записи, но если количество запросов больше, чем число вставки, это будет хорошо. Смотрите этот ответ за идею этого подхода:

    Query to get records based on Radius in SQLite?

  3. Вы можете посмотреть на что-то вроде geohashing.

При использовании в базе данных, структура данных geohashed имеет два преимущества. ,,, Во-вторых, эта структура индекса может быть использована для быстрого и грязного поиска близости - самые близкие точки часто входят в число ближайших геохэшей.

Вы можете искать SO некоторые идеи о том, как реализовать: https://stackoverflow.com/search?q=geohash

2

Первый запрос игнорирует параметры, заданные - с использованием 1 вместо @dist на расстоянии, и используя псевдоним таблицы orig вместо параметров @orig_lat и @orig_lon.

У вас есть запрос, делающий декартово произведение между таблицей и собой, что редко является хорошей идеей, если вы можете избежать этого. Вы уходите из-за состояния фильтра orig.id = 1, что означает, что есть только одна строка от orig, соединенная с каждой из строк в dest (включая точку с dest.id = 1; возможно, у вас должно быть условие AND orig.id != dest.id). У вас также есть предложение HAVING, но не предложение GROUP BY, что указывает на проблемы. Предложение HAVING не связывает какие-либо агрегаты, но предложение HAVING (в основном) для сравнения совокупных значений.

Если моя память не сработала, COS (ABS (x)) === COS (x), чтобы вы могли упростить ситуацию, сбросив ABS(). В противном случае неясно, почему одна широта нуждается в АБС, а другая - нет - симметрия имеет решающее значение в вопросах сферической тригонометрии.

У вас есть доза магических чисел - значение 69 - предположительно количество миль в градусе (долготы, на экваторе), а 3956 - радиус земли.

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

Условие dest.id = 1 во втором запросе нечетное; Я считаю, что его следует опустить, но его присутствие должно ускорить процесс, потому что только одно число соответствует этому условию. Поэтому дополнительное время озадачивает. Но использование индекса первичного ключа подходит как написано.

Вы должны переместить условие в предложении HAVING в предложение WHERE.

Но я не уверен, что это действительно помогает ...

+0

Хорошо заметили, не осознавали ошибки abs() и WHERE на втором! Спасибо, и я обновил сообщение соответственно. –

1

NGS Интернет Обратный Геодезический Калькулятором является традиционной ссылка означает, чтобы вычислить расстояние между любыми двумя точками на земном эллипсоиде:

http://www.ngs.noaa.gov/cgi-bin/Inv_Fwd/inverse2.prl

Но выше калькулятор по-прежнему проблематичен. Особенно между двумя соседними антиподами, расчетное расстояние может показаться ошибкой в ​​несколько десятков километров !!! Происхождение числовых проблем было идентифицировано давным-давно Таддеус Винсенти (стр 92):

http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf

В любом случае, это preferrable использовать надежный и очень точный онлайн калькулятор Чарльз Карни:

http://geographiclib.sourceforge.net/cgi-bin/Geod

0

Если вас интересуют только небольшие расстояния, вы можете приблизить географическую сетку прямоугольной сеткой.

SELECT *, SQRT(POWER(RADIANS(@mylat - dest.lat), 2) + 
       POWER(RADIANS(@mylon - dst.lng)*COS(RADIANS(@mylat)), 2) 
      )*@radiusOfEarth AS approximateDistance 
… 

Вы можете сделать это еще более эффективным, сохранив радианы вместо (или в дополнение) градусов в вашей базе данных. Если ваши запросы могут пересекаться с меридианом на 180 °, здесь потребуется дополнительная осторожность, но многие приложения не должны иметь дело с этими местоположениями. Кроме того, можно попытаться изменить POWER(х) в х*х, которые могли бы быть вычислен быстрее.