Oracle пространственный поиск на расстоянии - PullRequest
4 голосов
/ 30 января 2012

У меня есть следующие таблицы Города:

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

Теперь, основываясь на долготе пользователя (например, 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';

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

P.S: Если возможно иметь расстояние и быть отсортированным

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

Ответы [ 4 ]

16 голосов
/ 30 января 2012

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

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

Вот запрос, который поможет. Это использует расстояния в уставных милях. РЕДАКТИРОВАТЬ Это исправляет ошибку, упомянутую 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

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

           (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/

3 голосов
/ 19 мая 2012

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

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, поэтому мы имеем порядка миллиардов вызовов функций.

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

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 голосов
/ 08 октября 2012

Если вы действительно хотите использовать 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 для такой простой задачи является излишним. В этом случае я склонен согласиться, потому что в предложении WHERE вы можете сделать простой бокс, чтобы вырезать города не в прямоугольной рамке с центром вашей начальной точки и размером расстояния поиска; однако иногда вам понадобится информация об индексе R-дерева. Во всяком случае, их решения имеют 2 основные проблемы:

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

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

1 голос
/ 24 февраля 2017

Через несколько лет после принятого ответа можно добавить некоторые улучшения в запрос: в базе данных 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
...