Dette er en stort set forenklet version af en funktion, jeg bruger i en app, der blev bygget for omkring 3 år siden. Tilpasset det aktuelle spørgsmål.
-
Finder steder i omkredsen af et punkt ved hjælp af en boks . Man kunne gøre dette med en cirkel for at få mere præcise resultater, men dette er kun ment som en tilnærmelse til at begynde med.
-
Ignorerer det faktum, at verden ikke er flad. Min ansøgning var kun beregnet til en lokal region, et par 100 kilometer på tværs. Og søgeomkredsen strækker sig kun over et par kilometer på tværs. At gøre verden flad er godt nok til formålet. (Todo:En bedre tilnærmelse af forholdet lat/lon afhængigt af geoplaceringen kan hjælpe.)
-
Fungerer med geokoder, som du får fra Google maps.
-
Virker med standard PostgreSQL uden udvidelse (ingen PostGis påkrævet), testet på PostgreSQL 9.1 og 9.2.
Uden indeks ville man skulle beregne afstanden for hver række i basistabellen og filtrere de nærmeste. Ekstremt dyrt med store borde.
Rediger:
Jeg tjekkede igen, og den nuværende implementering tillader et GisT-indeks på point (Postgres 9.1 eller nyere). Forenklet koden i overensstemmelse hermed.
Det store trick er at bruge et funktionelt GiST-indeks over kasser , selvom klummen kun er en pointe. Dette gør det muligt at bruge den eksisterende GiST-implementering
.
Med sådan en (meget hurtig) søgning kan vi få alle lokationer inde i en boks. Det resterende problem:vi kender antallet af rækker, men vi kender ikke størrelsen på den kasse, de er i. Det er som at kende en del af svaret, men ikke spørgsmålet.
Jeg bruger et lignende omvendt opslag tilgang til den, der er beskrevet mere detaljeret i dette relaterede svar på dba.SE . (Kun, jeg bruger ikke delvise indekser her - måske virker det også).
Gentag gennem en række foruddefinerede søgetrin, fra meget små op til "lige store nok til at rumme mindst nok placeringer". Betyder, at vi skal køre et par (meget hurtige) forespørgsler for at komme til størrelsen på søgefeltet.
Søg derefter i basistabellen med denne boks, og beregn den faktiske afstand for kun de få rækker, der returneres fra indekset. Der vil normalt være noget overskud, da vi fandt boksen med mindst nok steder. Ved at tage de nærmeste runder vi effektivt boksens hjørner. Du kan fremtvinge denne effekt ved at gøre boksen et hak større (multiplicer radius
i funktionen af sqrt(2) for at blive fuldstændig præcis resultater, men jeg vil ikke gå helt ud, da dette er omtrentligt til at begynde med).
Dette ville være endnu hurtigere og enklere med en SP GiST indeks, tilgængelig i den seneste version af PostgreSQL. Men jeg ved ikke om det er muligt endnu. Vi skulle have en faktisk implementering af datatypen, og jeg havde ikke tid til at dykke ned i det. Hvis du finder en måde, lover du at rapportere tilbage!
Givet denne forenklede tabel med nogle eksempelværdier (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)');
Indekset ser således ud:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
Du bliver nødt til at justere hjemmeområdet, trinene og skaleringsfaktoren til dine behov. Så længe du søger i kasser på et par kilometer omkring et punkt, er en flad jord en god nok tilnærmelse.
Du skal forstå plpgsql godt for at arbejde med dette. Jeg føler, jeg har gjort nok her.
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;
Ring til:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Returnerer en liste med $3
steder, hvis der er nok i det definerede maksimale søgeområde.
Sorteret efter faktisk afstand.
Yderligere forbedringer
Byg en funktion som:
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) --';
De (bogstaveligt talt) globale konstanter 111200
og 111400
er optimeret til mit område (Østrig) fra Længde af en længdegrad
og Længden af en breddegrad
, men i bund og grund bare arbejde over hele verden.
Brug den til at tilføje en skaleret geokode til basistabellen, ideelt set en genereret kolonne som beskrevet i dette svar:
Hvordan laver du dateringsmatematik, der ignorerer året?
Se 3. Sort magisk version hvor jeg leder dig gennem processen.
Så kan du forenkle funktionen noget mere:Skaler inputværdier én gang og fjern overflødige beregninger.