PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Wie kann ich Ergebnisse von einer JPA-Einheit nach Entfernung geordnet erhalten?

Dies ist eine stark vereinfachte Version einer Funktion, die ich in einer App verwende, die vor etwa 3 Jahren erstellt wurde. Angepasst an die jeweilige Fragestellung.

  • Sucht Orte im Umkreis eines Punktes mithilfe eines Felds . Man könnte dies mit einem Kreis tun, um genauere Ergebnisse zu erhalten, aber dies soll zunächst nur eine Annäherung sein.

  • Ignoriert die Tatsache, dass die Welt nicht flach ist. Meine Bewerbung war nur für eine lokale Region gedacht, einige 100 Kilometer im Durchmesser. Und der Suchradius erstreckt sich nur über wenige Kilometer. Die Welt flach zu machen ist gut genug für den Zweck. (Todo:Eine bessere Annäherung für das Verhältnis Breitengrad/Längengrad in Abhängigkeit von der Geolokalisierung könnte hilfreich sein.)

  • Funktioniert mit Geocodes, wie Sie sie von Google Maps erhalten.

  • Funktioniert mit Standard-PostgreSQL ohne Erweiterung (kein PostGis erforderlich), getestet auf PostgreSQL 9.1 und 9.2.

Ohne Index müsste man die Entfernung für jede Zeile in der Basistabelle berechnen und die nächsten herausfiltern. Extrem teuer bei großen Tischen.

Bearbeiten:
Ich habe es noch einmal überprüft und die aktuelle Implementierung erlaubt einen GisT-Index für Punkte (Postgres 9.1 oder höher). Vereinfachte den Code entsprechend.

Der große Trick ist die Verwendung eines funktionalen GiST-Index von Boxen , obwohl die Spalte nur ein Punkt ist. Dadurch ist es möglich, die vorhandene GiST-Implementierung zu verwenden .

Mit einer solchen (sehr schnellen) Suche können wir alle Orte in einer Box finden. Das verbleibende Problem:Wir kennen die Anzahl der Zeilen, aber wir kennen nicht die Größe des Kästchens, in dem sie sich befinden. Das ist so, als ob wir einen Teil der Antwort kennen, aber nicht die Frage.

Ich verwende ein ähnliches Reverse-Lookup Ansatz zu dem ausführlicher in diese verwandte Antwort auf dba.SE . (Nur verwende ich hier keine Teilindizes - könnte auch funktionieren).

Iterieren Sie durch eine Reihe vordefinierter Suchschritte, von sehr klein bis zu „gerade groß genug, um mindestens genügend Standorte aufzunehmen“. Das bedeutet, dass wir ein paar (sehr schnelle) Abfragen ausführen müssen, um die Größe für das Suchfeld zu ermitteln.

Durchsuchen Sie dann die Basistabelle mit diesem Feld und berechnen Sie die tatsächliche Entfernung nur für die wenigen Zeilen, die vom Index zurückgegeben werden. Es wird normalerweise etwas Überschuss geben, da wir die Kiste gefunden haben, die mindestens enthält genügend Standorte. Indem wir die nächsten nehmen, runden wir effektiv die Ecken der Box ab. Sie könnten diesen Effekt erzwingen, indem Sie die Box etwas größer machen (radius multiplizieren in der Funktion von sqrt(2), um vollständig genau zu werden Ergebnisse, aber ich würde nicht aufs Ganze gehen, da dies zunächst eine Annäherung ist).

Noch schneller und einfacher geht es mit einem SP GiST index, verfügbar in der neuesten Version von PostgreSQL. Aber ob das möglich ist, weiß ich noch nicht. Wir bräuchten eine tatsächliche Implementierung für den Datentyp, und ich hatte nicht die Zeit, mich damit zu beschäftigen. Wenn Sie einen Weg finden, versprechen Sie, sich zu melden!

Angesichts dieser vereinfachten Tabelle mit einigen Beispielwerten (adr .. Adresse):

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

Der Index sieht folgendermaßen aus:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Sie müssen den Heimatbereich, die Schritte und den Skalierungsfaktor an Ihre Bedürfnisse anpassen. Solange Sie in Kästen von einigen Kilometern um einen Punkt herum suchen, ist eine flache Erde eine ausreichend gute Annäherung.

Sie müssen plpgsql gut verstehen, um damit arbeiten zu können. Ich habe das Gefühl, hier genug getan zu haben.

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;

Aufruf:

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

Gibt eine Liste von $3 zurück Standorte, wenn genügend im definierten maximalen Suchbereich vorhanden sind.
Sortiert nach tatsächlicher Entfernung.

Weitere Verbesserungen

Erstellen Sie eine Funktion wie:

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

Die (buchstäblich) globalen Konstanten 111200 und 111400 sind für meine Region (Österreich) aus der Länge eines Längengrades optimiert und Die Länge eines Breitengrades , aber im Grunde nur auf der ganzen Welt arbeiten.

Verwenden Sie es, um der Basistabelle einen skalierten Geocode hinzuzufügen, idealerweise eine generierte Spalte wie in dieser Antwort beschrieben:
Wie macht man Datumsberechnungen, die das Jahr ignorieren?
Siehe 3. Schwarzmagische Version wo ich Sie durch den Prozess führe.
Dann können Sie die Funktion noch weiter vereinfachen:Eingabewerte einmal skalieren und überflüssige Berechnungen entfernen.