2012-01-30 2 views
4

У меня есть следующие города стола:Oracle Пространственный поиск в пределах расстояния

ID(int),City(char),latitude(float),longitude(float). 

Теперь на основе долготы `s (например: 44,8) и широты (например: 46,3) Я хочу, чтобы найти всех городов рядом с ним в пределах 100 миль/км.

Я нашел несколько примеров, но не знаю, как адаптировать их к моему делу

select * 
from GEO.Cities a 
where SDO_WITHIN_DISTANCE([I don`t know], 
MDSYS.SDO_GEOMETRY(2001, 8307, MDSYS.SDO_POINT_TYPE(44.8,46.3, NULL) ,NULL, NULL), 
'distance = 1000') = 'TRUE'; 

Любая помощь будет оценена.

PS: Если это возможно, чтобы расстояние и быть отсортирован

PPS: Я хочу сделать это таким образом из-за проблемы с производительностью, я сделал это таким образом http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL но это занимает слишком много времени ...

ответ

15

У вас есть неплохая ссылка для поиска расстояния mySQL.

Забудьте об Oracle Spatial. Слишком много кода, слишком много сложностей, недостаточно добавить значение.

Вот запрос, который сделает трюк. Это использует расстояния в мили устава. EDIT Это исправляет ошибку, упомянутую mdarwin, ценой проверки на разделение, если вы попытаетесь использовать ее для местоположения на северном или южном полюсе.

SELECT id, city, LATITUDE, LONGITUDE, distance 
    FROM 
    (
    SELECT id, 
      city, 
      LATITUDE, LONGITUDE, 
      (3959 * ACOS(COS(RADIANS(LATITUDE)) 
       * COS(RADIANS(mylat)) 
       * COS(RADIANS(LONGITUDE) - RADIANS(mylng)) 
       + SIN(RADIANS(LATITUDE)) 
       * SIN(RADIANS(mylat)) 
       )) 
      AS distance, 
      b.mydst 
     FROM Cities 
     JOIN (
     SELECT :LAT AS mylat, 
       :LONG AS mylng, 
       :RADIUS_LIMIT AS mydst 
      FROM DUAL 
    )b ON (1 = 1) 
    WHERE LATITUDE >= mylat -(mydst/69) 
     AND LATITUDE <= mylat +(mydst/69) 
     AND LONGITUDE >= mylng -(mydst/(69 * COS(RADIANS(mylat)))) 
     AND LONGITUDE <= mylng +(mydst/(69 * COS(RADIANS(mylat)))) 
)a 
    WHERE distance <= mydst 
    ORDER BY distance 

Если вы работаете в километрах, изменить mydst/69 для mydst/111,045, и изменить 3959 на 6371.4. (1/69 конвертирует мили в градусы, 3959 - значение радиуса планеты.)

Теперь у вас, вероятно, возникнет соблазн использовать этот большой запрос как «волшебный черный ящик». Не делай этого! Это не очень сложно понять, и если вы поймете это, вы сможете сделать лучшую работу. Вот что происходит.

Этот раздел является основой того, что делает запрос быстрым. Он выполняет поиск в таблице городов для близлежащих городов до указанной вами точки.

 WHERE LATITUDE >= mylat -(mydst/69) 
     AND LATITUDE <= mylat +(mydst/69) 
     AND LONGITUDE >= mylng -(mydst/(69 * COS(RADIANS(mylat)))) 
     AND LONGITUDE <= mylng +(mydst/(69 * COS(RADIANS(mylat)))) 

Для этого вам определенно нужен указатель в столбце LATITUDE. Индекс вашего столбца LONGITUDE также поможет немного. Он выполняет приблизительный поиск, ища строки, находящиеся внутри квази-прямоугольного патча на поверхности земли рядом с вашей точкой. Он выбирает слишком много городов, но не слишком много.

Это положение здесь позволяет исключить дополнительные города из набора результатов:

WHERE distance <= mydst 

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

  (3959 * ACOS(COS(RADIANS(LATITUDE)) 
       * COS(RADIANS(mylat)) 
       * COS(RADIANS(LONGITUDE) - RADIANS(mylng)) 
       + SIN(RADIANS(LATITUDE)) 
       * SIN(RADIANS(mylat)) 

Это предложение позволяет вводить вашу точку и ваш предел радиуса только один раз в качестве связанных переменных в ваш запрос. Это полезно, потому что различные формулы используют эти переменные несколько раз.

 SELECT :LAT AS mylat, 
       :LONG AS mylng, 
       :RADIUS_LIMIT AS mydst 
      FROM DUAL 

Остальная часть запроса просто упорядочивает вещи, поэтому вы выбираете и заказываете расстояние.

Вот более полное объяснение: http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

+0

Я продолжал делать что-то вроде этого, используя кэш .. . Я надеялся добиться лучшей производительности с Spatial ... по крайней мере, чтобы сделать сравнение, чтобы посмотреть, какой путь я должен взять:) ... Спасибо за то, что вы время –

+0

Spatial не имеет в этом никакого магии. Поиск индекса по столбцу с плавающей точкой (широта) будет иметь одинаковую сложность. –

+3

У этого есть некоторые магические индексы R-дерева, которые значительно отличаются от обычных (B-tree) индексов и предназначены для явного решения проблемы близости по двум измерениям. –

3

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

CREATE OR REPLACE Function CIC3.F_FLATEARTHRAD 
    (latoriginrad IN number, 
    longoriginrad IN number, 
    latdestrad IN number, 
    longdestrad IN number) 

RETURN number IS 
    a number; 
    b number; 
    c number; 
    u number; 
    v number; 

    HalfPi number:=1.5707963; 
    R number:=3956; 
BEGIN 
    if latoriginrad is null or latdestrad is null or 
    longdestrad is null or longoriginrad is null then 
     return null; 
    end if; 
    a := HalfPi - latoriginrad; 
    b := HalfPi - latdestrad; 
    u := a * a + b * b; 
    v := - 2 * a * b * cos(longdestrad - longoriginrad); 
    c := sqrt(abs(u + v)); 

    return R * c; 
END; 

Тогда ваш запрос становится

select * from GEO.Cities a 
where F_FLATEARTHRAD(44.8*0.0174,46.3*0.0174, 
       latitude_radians,longitude_radians)<1000 

необходим 0,0174 фактор, поскольку формула использует радианы не градусов. Поэтому вам нужно либо хранить радианы (возможно, с помощью триггера). Или вам нужно будет изменить формулу, чтобы принять градусы. В целях запросов вы можете запрашивать тысячи записей, и даже одно дополнительное умножение может повлиять на время ответа. В нашем случае некоторые запросы сравнивают расстояния между двумя таблицами 4k записей на одном и 200k, поэтому мы имеем порядка миллиардов вызовов функций.

Ниже приведен эквивалент таверны для людей, которым не нужно беспокоиться о времени.

CREATE OR REPLACE Function CIC3.F_HAVERSINE 
    (latorigin IN number, 
    longorigin IN number, 
    latdest IN number, 
    longdest IN number) 

    RETURN number IS 
    v_longoriginrad number; 
    v_latoriginrad number; 
    v_longdestrad number; 
    v_latdestrad number; 
    v_difflat number; 
    v_difflong number; 
    a number; 
    c number; 
    d number; 
    z number; 
    x number; 
    e number; 
    f number; 
    g number; 
    h number; 
    i number; 
    j number; 
    k number; 
    l number; 
    m number; 
    n number; 
    o number; 
    p number; 
    q number; 
    y number; 
BEGIN 
    z := .017453293; 
    x := 3956; 
    y := 57.295780; 
    v_longoriginrad:=longorigin*z; 
    v_latoriginrad:=latorigin*z; 
    v_longdestrad:=longdest*z; 
    v_latdestrad:=latdest*z; 
    v_difflong:=v_longdestrad-v_longoriginrad; 
    v_difflat:=v_latdestrad-v_latoriginrad; 

    j:=(v_difflat/2); 
    k:=sin(j); 
    l:=power(k,2); 

    m:=cos(v_latoriginrad); 

    n:=cos(v_latdestrad); 

    o:=v_difflong/2; 
    p:=sin(o); 
    q:=power(p,2); 

    a:=l+m*n*q; 

    c := 2 * asin(sqrt(a)); 

    d := x * c; 

    return d; 
END; 
2

Если вы действительно хотите использовать SDO_WITHIN_DISTANCE, необходимо создать столбец типа SDO_GEOMETRY в таблице городов, заполнить пространственные метаданные индекса и создать пространственный индекс:

  1. SDO_GEOMETRY колонка:

    CREATE TABLE MYTABLE(
    ..., 
    GEOLOC MDSYS.SDO_GEOMETRY, 
    ... 
    ); 
    
  2. пространственного индекса Метаданные:

    INSERT INTO USER_SDO_GEOM_METADATA (TABLE_NAME, COLUMN_NAME, DIMINFO, SRID) 
    VALUES ('MYTABLE' /*your table name*/, 'GEOLOC', /*your spatial column name*/ 
        SDO_DIM_ARRAY(SDO_DIM_ELEMENT('X', -180, 180, 1), 
           SDO_DIM_ELEMENT('Y', -90, 90, 1)), 
           8307); 
    
  3. Создать пространственный индекс:

    CREATE INDEX MY_SPATIAL_IDX ON MYTABLE (GEOLOC) 
    tablespace SomeTablespace; -- optional 
    
  4. Теперь заменить GEOLOC где вы сказали, [я не знаю].

Это должно было ответить на ваш вопрос. Другие дали вам намек на то, что использование Oracle space для такой простой задачи происходит от излишнего. В этом случае я склонен согласиться, потому что вы можете сделать простой бокс в предложении WHERE, чтобы вырезать города не в прямоугольной коробке с центром вашей начальной точки и размером вашего расстояния поиска; однако иногда вам нужен интеллект индекса R-дерева. Во всяком случае, их решения имеют две основные проблемы:

a. Они используют метод Great Circle для вычисления расстояния между точками. Это слишком грубо, вам нужно использовать эллипсоидный подход, чтобы получить более точные результаты. Googling дает ответ сразу, как this.

b. Если вы запрограммируете алгоритм расстояния эллипсоида в PL/SQL, вы найдете его слишком медленным. Решение состоит в том, чтобы переместить эту логику на Java или C++ и сделать ее вызываемой из Oracle (есть стандартный способ сделать это).

0

Через несколько лет после принятого ответа можно добавить некоторые улучшения в запрос: База данных Oracle в версии 11.1 добавила функцию calc_distance (http://psoug.org/reference/functions.html), полезную для точного расчета расстояния.
О предложениях, чтобы быстрее сделать запрос, использует константу преобразования от расстояния до радианов, которая изменяется в зависимости от широты (http://www.longitudestore.com/how-big-is-one-gps-degree.html) и добавляет ошибку, которая увеличивается с радиусом поиска.

Вот мои изменения, которые использует среднее значение радиуса Земли, в моих тестах, кажется, чтобы быть более точным для больших радиусов поиска, в Европе широтах:

SELECT id, city, LATITUDE, LONGITUDE, distance FROM 
    (
    SELECT id, 
      city, 
      LATITUDE, LONGITUDE, 
      calc_distance(LATITUDE, LONGITUDE, mylat, mylng) AS distance, 
      b.mydst 
     FROM Cities 
     JOIN (
     SELECT :LAT AS mylat, 
       :LONG AS mylng, 
       :RADIUS_LIMIT AS mydst, 
       3.1415926 AS pi, -- or use pi() function if available 
       6371.4 earthradius 
      FROM DUAL 
    )b ON (1 = 1) 
    WHERE LATITUDE >= mylat - ((mydst/earthradius) * (180/pi)) 
     AND LATITUDE <= mylat + ((mydst/earthradius) * (180/pi)) 
     AND LONGITUDE >= mylng - ((mydst/earthradius) * (180/pi)/cos(mylat * pi/180)) 
     AND LONGITUDE <= mylng + ((mydst/earthradius) * (180/pi)/cos(mylat * pi/180)) 
)a 
WHERE distance <= mydst 
ORDER BY distance