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