PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Come posso ottenere risultati da un'entità JPA ordinata per distanza?

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

-> SQLfiddle

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.