PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Как мога да получа резултати от JPA обект, подредени по разстояние?

Това е до голяма степен опростена версия на функция, която използвам в приложение, създадено преди около 3 години. Адаптирано към разглеждания въпрос.

  • Намира местоположения в периметъра на точка с помощта на кутия . Човек може да направи това с кръг, за да получи по-точни резултати, но това е предназначено да бъде само приблизително като начало.

  • Пренебрегва факта, че светът не е плосък. Приложението ми беше предназначено само за местен регион, няколко 100 километра. А периметърът на търсене се простира само на няколко километра. Да направим света плосък е достатъчно добро за целта. (Задача:По-добро приближение за съотношението ширина/дължина в зависимост от геолокацията може да помогне.)

  • Работи с геокодове, каквито получавате от Google Maps.

  • Работи със стандартния PostgreSQL без разширение (не се изисква PostGis), тестван на PostgreSQL 9.1 и 9.2.

Без индекс би трябвало да се изчисли разстоянието за всеки ред в базовата таблица и да се филтрират най-близките. Изключително скъпо с големи маси.

Редактиране:
Проверих отново и текущата реализация позволява GisT индекс на точки (Postgres 9.1 или по-нова). Съответно опростете кода.

Основният трик е да се използва функционален GiST индекс на кутии , въпреки че колоната е само точка. Това прави възможно използването на съществуващата имплементация на GiST .

С такова (много бързо) търсене можем да получим всички местоположения в кутия. Оставащият проблем:знаем броя на редовете, но не знаем размера на кутията, в която се намират. Това е все едно да знаем част от отговора, но не и въпроса.

Използвам подобно обратно търсене подход към описания по-подробно в този свързан отговор на dba.SE . (Само, че не използвам частични индекси тук - може също да работи).

Преминете през набор от предварително дефинирани стъпки за търсене, от много малки до „достатъчно големи, за да поберат поне достатъчно местоположения“. Означава, че трябва да изпълним няколко (много бързи) заявки, за да стигнем до размера на полето за търсене.

След това потърсете в базовата таблица с това поле и изчислете действителното разстояние само за няколкото реда, върнати от индекса. Обикновено ще има излишък, тъй като открихме, че кутията съдържа поне достатъчно места. Като вземаме най-близките, ефективно закръгляме ъглите на кутията. Бихте могли да принудите този ефект, като направите полето с един прорез по-голям (умножете radius във функцията от sqrt(2), за да получите напълно точно резултати, но не бих давал всичко, тъй като това е приблизително като начало).

Това би било още по-бързо и по-просто с SP GiST индекс, наличен в най-новата версия на PostgreSQL. Но все още не знам дали това е възможно. Трябваше ни действително внедряване за типа данни и нямах време да се потопя в него. Ако намерите начин, обещайте да докладвате!

Предвид тази опростена таблица с някои примерни стойности (adr .. адрес):

CREATE TABLE adr(adr_id int, adr text, geocode point);
INSERT INTO adr (adr_id, adr, geocode) VALUES
    (1,  'adr1', '(48.20117,16.294)'),
    (2,  'adr2', '(48.19834,16.302)'),
    (3,  'adr3', '(48.19755,16.299)'),
    (4,  'adr4', '(48.19727,16.303)'),
    (5,  'adr5', '(48.19796,16.304)'),
    (6,  'adr6', '(48.19791,16.302)'),
    (7,  'adr7', '(48.19813,16.304)'),
    (8,  'adr8', '(48.19735,16.299)'),
    (9,  'adr9', '(48.19746,16.297)');

Индексът изглежда така:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Ще трябва да настроите домашната зона, стъпките и коефициента на мащабиране според вашите нужди. Докато търсите в кутии от няколко километра около точка, плоската земя е достатъчно добро приближение.

Трябва да разбирате добре plpgsql, за да работите с това. Чувствам, че направих достатъчно тук.

CREATE OR REPLACE FUNCTION f_find_around(_lat double precision, _lon double precision, _limit bigint = 50)
  RETURNS TABLE(adr_id int, adr text, distance int) AS
$func$
DECLARE
   _homearea   CONSTANT box := '(49.05,17.15),(46.35,9.45)'::box;      -- box around legal area
-- 100m = 0.0008892                   250m, 340m, 450m, 700m,1000m,1500m,2000m,3000m,4500m,7000m
   _steps      CONSTANT real[] := '{0.0022,0.003,0.004,0.006,0.009,0.013,0.018,0.027,0.040,0.062}';  -- find optimum _steps by experimenting
   geo2m       CONSTANT integer := 73500;                              -- ratio geocode(lon) to meter (found by trial & error with google maps)
   lat2lon     CONSTANT real := 1.53;                                  -- ratio lon/lat (lat is worth more; found by trial & error with google maps in (Vienna)
   _radius     real;                                                   -- final search radius
   _area       box;                                                    -- box to search in
   _count      bigint := 0;                                            -- count rows
   _point      point := point($1,$2);                                  -- center of search
   _scalepoint point := point($1 * lat2lon, $2);                       -- lat scaled to adjust
BEGIN

 -- Optimize _radius
IF (_point <@ _homearea) THEN
   FOREACH _radius IN ARRAY _steps LOOP
      SELECT INTO _count  count(*) FROM adr a
      WHERE  a.geocode <@ box(point($1 - _radius, $2 - _radius * lat2lon)
                            , point($1 + _radius, $2 + _radius * lat2lon));

      EXIT WHEN _count >= _limit;
   END LOOP;
END IF;

IF _count = 0 THEN                                                     -- nothing found or not in legal area
   EXIT;
ELSE
   IF _radius IS NULL THEN
      _radius := _steps[array_upper(_steps,1)];                        --  max. _radius
   END IF;
   _area := box(point($1 - _radius, $2 - _radius * lat2lon)
              , point($1 + _radius, $2 + _radius * lat2lon));
END IF;

RETURN QUERY
SELECT a.adr_id
      ,a.adr
      ,((point (a.geocode[0] * lat2lon, a.geocode[1]) <-> _scalepoint) * geo2m)::int4 AS distance
FROM   adr a
WHERE  a.geocode <@ _area
ORDER  BY distance, a.adr, a.adr_id
LIMIT  _limit;

END
$func$  LANGUAGE plpgsql;

Обаждане:

SELECT * FROM f_find_around (48.2, 16.3, 20);

Връща списък от $3 местоположения, ако има достатъчно в определената максимална зона за търсене.
Сортирани по действително разстояние.

Допълнителни подобрения

Създайте функция като:

CREATE OR REPLACE FUNCTION f_geo2m(double precision, double precision)
  RETURNS point AS
$BODY$
SELECT point($1 * 111200, $2 * 111400 * cos(radians($1)));
$BODY$
  LANGUAGE sql IMMUTABLE;

COMMENT ON FUNCTION f_geo2m(double precision, double precision)
IS 'Project geocode to approximate metric coordinates.
    SELECT f_geo2m(48.20872, 16.37263)  --';

(Буквално) глобалните константи 111200 и 111400 са оптимизирани за моя район (Австрия) от Дължина на градус дължина и Дължината на градус географска ширина , но по същество просто работят по целия свят.

Използвайте го, за да добавите мащабиран геокод към основната таблица, в идеалния случай генерирана колона както е описано в този отговор:
Как се прави математика за дата, която игнорира годината?
Вижте 3. Черна магическа версията където ви превеждам през процеса.
След това можете да опростите функцията още малко:мащабирайте входните стойности веднъж и премахнете излишните изчисления.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да търсите enum в списък с низове чрез заявка postgresql?

  2. Данни за пари в PostgreSQL с помощта на Java

  3. Postgresql пълнотекстово търсене за чешки език (няма езикова конфигурация по подразбиране)

  4. Проблем при извличане на записи с празен масив

  5. Hibernate Distinct с подреждане по