PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Jak mogę uzyskać wyniki od podmiotu JPA uporządkowane według odległości?

Jest to w dużej mierze uproszczona wersja funkcji, której używam w aplikacji zbudowanej około 3 lata temu. Dostosowane do aktualnego pytania.

  • Znajduje lokalizacje na obwodzie punktu za pomocą pudełka . Można to zrobić za pomocą koła, aby uzyskać dokładniejsze wyniki, ale na początku ma to być tylko przybliżenie.

  • Ignoruje fakt, że świat nie jest płaski. Moja aplikacja była przeznaczona tylko dla regionu o średnicy kilkuset kilometrów. A obwód poszukiwawczy obejmuje tylko kilka kilometrów. Wyrównywanie świata jest wystarczająco dobre do tego celu. (Do zrobienia:może pomóc lepsze przybliżenie stosunku lat/dług w zależności od geolokalizacji).

  • Działa z geokodami, tak jak w mapach Google.

  • Działa ze standardowym PostgreSQL bez rozszerzenia (nie wymaga PostGis), przetestowane na PostgreSQL 9.1 i 9.2.

Bez indeksu należałoby obliczyć odległość dla każdego wiersza w tabeli bazowej i przefiltrować najbliższe. Niezwykle drogie z dużymi stołami.

Edytuj:
Sprawdziłem ponownie i bieżąca implementacja pozwala na indeks GisT dla punktów (Postgres 9.1 lub nowszy). Odpowiednio uproszczono kod.

główna sztuczka jest użycie funkcjonalnego indeksu GiST pudełek , mimo że kolumna to tylko punkt. Umożliwia to korzystanie z istniejącej implementacji GiST .

Przy takim (bardzo szybkim) wyszukiwaniu możemy uzyskać wszystkie lokalizacje wewnątrz pudełka. Pozostały problem:znamy liczbę rzędów, ale nie znamy rozmiaru pudełka, w którym się znajdują. To tak, jakby znać część odpowiedzi, ale nie pytanie.

Używam podobnego odwrotnego wyszukiwania podejście do opisanego bardziej szczegółowo w ta powiązana odpowiedź na dba.SE . (Tylko, że nie używam tutaj częściowych indeksów - może też działać).

Przejdź przez szereg wstępnie zdefiniowanych kroków wyszukiwania, od bardzo małych do „wystarczająco dużych, aby pomieścić co najmniej wystarczającą liczbę lokalizacji”. Oznacza to, że musimy uruchomić kilka (bardzo szybkich) zapytań, aby uzyskać rozmiar pola wyszukiwania.

Następnie przeszukaj tabelę bazową za pomocą tego pola i oblicz rzeczywistą odległość tylko dla kilku wierszy zwróconych z indeksu. Zwykle będzie trochę nadwyżki, ponieważ znaleźliśmy pudełko zawierające co najmniej wystarczająca liczba lokalizacji. Wybierając te najbliższe skutecznie zaokrąglamy rogi pudełka. Możesz wymusić ten efekt, powiększając pole o jeden oczko (pomnóż radius w funkcji sqrt(2), aby uzyskać pełną dokładność wyniki, ale nie wyszedłbym na całość, ponieważ na początku jest to przybliżone).

Byłoby to jeszcze szybsze i prostsze dzięki SP GiST index, dostępny w najnowszej wersji PostgreSQL. Ale nie wiem, czy to jeszcze możliwe. Potrzebowalibyśmy rzeczywistej implementacji tego typu danych, a ja nie miałem czasu się w nią zagłębiać. Jeśli znajdziesz sposób, obiecaj się zgłosić!

Biorąc pod uwagę tę uproszczoną tabelę z kilkoma przykładowymi wartościami (adr .. adres):

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

Indeks wygląda tak:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Będziesz musiał dostosować obszar domu, stopnie i współczynnik skalowania do swoich potrzeb. Dopóki przeszukujesz pola o długości kilku kilometrów wokół punktu, płaska ziemia jest wystarczającym przybliżeniem.

Aby z tym pracować, musisz dobrze zrozumieć plpgsql. Czuję, że zrobiłem tutaj wystarczająco dużo.

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;

Zadzwoń:

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

Zwraca listę $3 lokalizacje, jeśli jest ich wystarczająco dużo w zdefiniowanym maksymalnym obszarze wyszukiwania.
Posortowane według rzeczywistej odległości.

Dalsze ulepszenia

Zbuduj funkcję taką jak:

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

(dosłownie) globalne stałe 111200 i 111400 są zoptymalizowane dla mojego obszaru (Austria) na podstawie Długość stopnia długości geograficznej oraz Długość stopnia szerokości geograficznej , ale w zasadzie działa na całym świecie.

Użyj go, aby dodać skalowany geokod do tabeli podstawowej, najlepiej wygenerowaną kolumnę jak opisano w tej odpowiedzi:
Jak wykonać obliczenia na datach, które ignorują rok?
Patrz 3. Wersja czarnej magii gdzie przeprowadzę Cię przez ten proces.
Następnie możesz uprościć funkcję jeszcze bardziej:Skaluj wartości wejściowe raz i usuń zbędne obliczenia.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Klucze obce POSTGRESQL Odwołujące się do kluczy podstawowych dwóch różnych tabel

  2. Przekazać parametry WHERE do widoku PostgreSQL?

  3. Hibernacja :Wyodrębnij kolumnę zawierającą dane binarne, które nie powinny być ładowane

  4. SQL dwa kryteria z jednego group-by

  5. Czy można używać zmiennej i nie określać typu zwracanego w postgreSQL?