Questa è una versione ampiamente semplificata di una funzione che utilizzo in un'app creata circa 3 anni fa. Adattato alla domanda in questione.
-
Trova le posizioni nel perimetro di un punto utilizzando una casella . Si potrebbe farlo con un cerchio per ottenere risultati più accurati, ma questa è solo un'approssimazione per cominciare.
-
Ignora il fatto che il mondo non è piatto. La mia domanda era destinata solo a una regione locale, di pochi 100 chilometri di diametro. E il perimetro di ricerca si estende solo per pochi chilometri. Rendere il mondo piatto è abbastanza buono per lo scopo. (Todo:una migliore approssimazione del rapporto lat/lon a seconda della geolocalizzazione potrebbe essere d'aiuto.)
-
Funziona con i geocodici come quelli di Google Maps.
-
Funziona con PostgreSQL standard senza estensione (non è richiesto PostGis), testato su PostgreSQL 9.1 e 9.2.
Senza indice, si dovrebbe calcolare la distanza per ogni riga nella tabella di base e filtrare quelle più vicine. Estremamente costoso con tavoli grandi.
Modifica:
Ho ricontrollato e l'attuale implementazione consente un indice GisT sui punti (Postgres 9.1 o successivo). Semplificato il codice di conseguenza.
Il trucco principale consiste nell'utilizzare un indice GiST di caselle funzionale , anche se la colonna è solo un punto. Ciò rende possibile utilizzare l'implementazione GiST
esistente .
Con una ricerca così (molto veloce), possiamo ottenere tutte le posizioni all'interno di una casella. Il problema rimanente:conosciamo il numero di righe, ma non conosciamo la dimensione della casella in cui si trovano. È come conoscere parte della risposta, ma non la domanda.
Uso una simile ricerca inversa approccio a quello descritto più dettagliatamente in questa risposta correlata su dba.SE . (Solo che non sto usando indici parziali qui - potrebbe anche funzionare davvero).
Esegui l'iterazione attraverso una serie di passaggi di ricerca predefiniti, da molto piccoli fino a "abbastanza grandi da contenere almeno un numero sufficiente di posizioni". Significa che dobbiamo eseguire un paio di query (molto veloci) per ottenere la dimensione della casella di ricerca.
Quindi cerca nella tabella di base con questa casella e calcola la distanza effettiva solo per le poche righe restituite dall'indice. Di solito ci sarà un po' di eccedenza poiché abbiamo trovato la scatola con almeno abbastanza posizioni. Prendendo quelli più vicini, giriamo efficacemente gli angoli della scatola. Puoi forzare questo effetto allargando di una tacca la scatola (moltiplicare radius
nella funzione di sqrt(2) per ottenere completamente preciso risultati, ma non farei di tutto, dal momento che questo è approssimativo per cominciare).
Questo sarebbe ancora più veloce e semplice con un SP GiST index, disponibile nell'ultima versione di PostgreSQL. Ma non so se è ancora possibile. Avremmo bisogno di un'implementazione effettiva per il tipo di dati e non ho avuto il tempo di approfondire. Se trovi un modo, prometti di riferire!
Data questa tabella semplificata con alcuni valori di esempio (adr
.. indirizzo):
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)');
L'indice si presenta così:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
Dovrai adattare l'area di casa, i gradini e il fattore di scala alle tue esigenze. Finché cerchi in caselle di pochi chilometri attorno a un punto, una terra piatta è un'approssimazione abbastanza buona.
Devi capire bene plpgsql per lavorare con questo. Sento di aver fatto abbastanza qui.
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;
Chiama:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Restituisce un elenco di $3
posizioni, se ce ne sono sufficienti nell'area di ricerca massima definita.
Ordinato per distanza effettiva.
Ulteriori miglioramenti
Crea una funzione come:
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) --';
Le (letteralmente) costanti globali 111200
e 111400
sono ottimizzati per la mia zona (Austria) dalla Lunghezza di un grado di longitudine
e La lunghezza di un grado di latitudine
, ma in pratica funziona in tutto il mondo.
Usalo per aggiungere un geocodice in scala alla tabella di base, idealmente una colonna generata come descritto in questa risposta:
Come fai a fare matematica con gli appuntamenti che ignora l'anno?
Fare riferimento a 3. Versione in magia nera dove ti guido attraverso il processo.
Quindi puoi semplificare ulteriormente la funzione:ridimensiona i valori di input una volta e rimuovi i calcoli ridondanti.