Dit is een grotendeels vereenvoudigde versie van een functie die ik gebruik in een app die ongeveer 3 jaar geleden is gebouwd. Aangepast aan de vraag.
-
Vindt locaties in de omtrek van een punt met behulp van een box . Je zou dit met een cirkel kunnen doen om nauwkeurigere resultaten te krijgen, maar dit is in het begin slechts een benadering.
-
Negeert het feit dat de wereld niet plat is. Mijn aanvraag was alleen bedoeld voor een lokale regio, een paar honderd kilometer breed. En het zoekgebied is maar een paar kilometer breed. De wereld plat maken is daarvoor goed genoeg. (Todo:een betere benadering voor de verhouding lat/lon, afhankelijk van de geolocatie, zou kunnen helpen.)
-
Werkt met geocodes zoals u die van Google maps krijgt.
-
Werkt met standaard PostgreSQL zonder extensie (geen PostGis vereist), getest op PostgreSQL 9.1 en 9.2.
Zonder index zou men de afstand voor elke rij in de basistabel moeten berekenen en de dichtstbijzijnde moeten filteren. Extreem duur met grote tafels.
Bewerken:
Ik heb het opnieuw gecontroleerd en de huidige implementatie staat een GisT-index op punten toe (Postgres 9.1 of hoger). Vereenvoudigde de code dienovereenkomstig.
De grote truc is het gebruik van een functionele GiST-index van boxes , ook al is de kolom slechts een punt. Dit maakt het mogelijk om de bestaande GiST-implementatie
te gebruiken .
Met zo'n (zeer snelle) zoekopdracht kunnen we alle locaties in een doos krijgen. Het resterende probleem:we weten het aantal rijen, maar we weten niet de grootte van het vak waarin ze zich bevinden. Dat is hetzelfde als een deel van het antwoord kennen, maar niet de vraag.
Ik gebruik een vergelijkbare reverse-lookup benadering van degene die in meer detail wordt beschreven in dit gerelateerde antwoord op dba.SE . (Alleen, ik gebruik hier geen gedeeltelijke indexen - zou ook kunnen werken).
Doorloop een reeks vooraf gedefinieerde zoekstappen, van heel klein tot "net groot genoeg om op zijn minst voldoende locaties te bevatten". Betekent dat we een aantal (zeer snelle) zoekopdrachten moeten uitvoeren om de grootte voor het zoekvak te krijgen.
Doorzoek vervolgens de basistabel met dit vak en bereken de werkelijke afstand voor alleen de paar rijen die uit de index worden geretourneerd. Er zal meestal wat overschot zijn, aangezien we de doos hebben gevonden met minstens voldoende locaties. Door de dichtstbijzijnde te nemen, ronden we effectief de hoeken van de doos af. Je zou dit effect kunnen forceren door het vak een tandje groter te maken (vermenigvuldig radius
in de functie door sqrt(2) om volledig nauwkeurig te krijgen resultaten, maar ik zou er niet helemaal voor gaan, aangezien dit in het begin bij benadering is).
Dit zou nog sneller en eenvoudiger zijn met een SP GiST index, beschikbaar in de nieuwste versie van PostgreSQL. Maar ik weet niet of dat nog kan. We hadden een daadwerkelijke implementatie nodig voor het gegevenstype en ik had niet de tijd om erin te duiken. Als je een manier vindt, beloof dan om terug te rapporteren!
Gezien deze vereenvoudigde tabel met enkele voorbeeldwaarden (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)');
De index ziet er als volgt uit:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
U moet het thuisgebied, de stappen en de schaalfactor aanpassen aan uw behoeften. Zolang je in hokjes van een paar kilometer rond een punt zoekt, is een platte aarde een goede benadering.
Je moet plpgsql goed begrijpen om hiermee te kunnen werken. Ik heb het gevoel dat ik hier genoeg heb gedaan.
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;
Bel:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Retourneert een lijst met $3
locaties, als er voldoende zijn in het gedefinieerde maximale zoekgebied.
Gesorteerd op werkelijke afstand.
Verdere verbeteringen
Bouw een functie zoals:
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 (letterlijk) globale constanten 111200
en 111400
zijn geoptimaliseerd voor mijn gebied (Oostenrijk) van de Lengte van een lengtegraad
en De lengte van een breedtegraad
, maar werk eigenlijk gewoon over de hele wereld.
Gebruik het om een geschaalde geocode toe te voegen aan de basistabel, idealiter een gegenereerde kolom zoals beschreven in dit antwoord:
Hoe reken je datums uit waarbij het jaartal wordt genegeerd?
Raadpleeg 3. Zwarte magie versie waar ik je door het proces begeleid.
Dan kun je de functie nog wat vereenvoudigen:schaal invoerwaarden één keer en verwijder overbodige berekeningen.