PostGIS punti più vicini con ST_Distance, kNN


23

Ho bisogno di ottenere su ogni elemento su una tabella il punto più vicino di un'altra tabella. La prima tabella contiene segnali stradali e la seconda le sale d'ingresso della città. Il fatto è che non riesco a usare la funzione ST_ClosestPoint e devo usare la funzione ST_Distance e ottenere il record minimo (ST_distance), ma sono piuttosto bloccato nella creazione della query.

CREATE TABLE traffic_signs
(
  id numeric(8,0) ),
  "GEOMETRY" geometry,
  CONSTRAINT traffic_signs_pkey PRIMARY KEY (id),
  CONSTRAINT traffic_signs_id_key UNIQUE (id)
)
WITH (
  OIDS=TRUE
);

CREATE TABLE entrance_halls
(
  id numeric(8,0) ),
  "GEOMETRY" geometry,
  CONSTRAINT entrance_halls_pkey PRIMARY KEY (id),
  CONSTRAINT entrance_halls_id_key UNIQUE (id)
)
WITH (
  OIDS=TRUE
);

Devo ottenere l'ID del entrnce_hall più vicino di ogni traffic_sign.

La mia domanda finora:

SELECT senal.id,port.id,ST_Distance(port."GEOMETRY",senal."GEOMETRY")  as dist
    FROM traffic_signs As senal, entrance_halls As port   
    ORDER BY senal.id,port.id,ST_Distance(port."GEOMETRY",senal."GEOMETRY")

Con questo sto ottenendo la distanza da ogni traffic_sign a ogni entrance_hall. Ma come posso ottenere solo la distanza minima?

Saluti,


Quale versione di PostgreSQL?
Jakub Kania,

Risposte:


41

Ci sei quasi. C'è un piccolo trucco che consiste nell'utilizzare l' operatore distinto di Postgres , che restituirà la prima partita di ogni combinazione - come stai ordinando da ST_Distance, effettivamente restituirà il punto più vicino da ogni senale a ciascuna porta.

SELECT 
   DISTINCT ON (senal.id) senal.id, port.id, ST_Distance(port."GEOMETRY", senal."GEOMETRY")  as dist
FROM traffic_signs As senal, entrance_halls As port   
ORDER BY senal.id, port.id, ST_Distance(port."GEOMETRY", senal."GEOMETRY");

Se sai che la distanza minima in ciascun caso non è superiore a una certa quantità x, (e hai un indice spaziale sui tuoi tavoli), puoi accelerare inserendo un WHERE ST_DWithin(port."GEOMETRY", senal."GEOMETRY", distance), ad esempio, se si sa che tutte le distanze minime sono non più di 10 km, quindi:

SELECT 
   DISTINCT ON (senal.id) senal.id, port.id, ST_Distance(port."GEOMETRY", senal."GEOMETRY")  as dist
FROM traffic_signs As senal, entrance_halls As port  
WHERE ST_DWithin(port."GEOMETRY", senal."GEOMETRY", 10000) 
ORDER BY senal.id, port.id, ST_Distance(port."GEOMETRY", senal."GEOMETRY");

Ovviamente, questo deve essere usato con cautela, come se la distanza minima sia maggiore, semplicemente non otterrai alcuna fila per quella combinazione di senale e porto.

Nota: l'ordine per ordine deve corrispondere al distinto sull'ordine, il che ha senso, poiché distinto sta prendendo il primo gruppo distinto in base ad un certo ordinamento.

Si presume che tu abbia un indice spaziale su entrambe le tabelle.

MODIFICA 1 . Esiste un'altra opzione, che consiste nell'utilizzare gli operatori <-> e <#> di Postgres (rispettivamente i calcoli della distanza del punto centrale e del riquadro di selezione) che fanno un uso più efficiente dell'indice spaziale e non richiedono l'hack ST_DWithin per evitare n ^ 2 confronti. C'è un buon articolo sul blog che spiega come funzionano. La cosa generale da notare è che questi due operatori lavorano nella clausola ORDER BY.

SELECT senal.id, 
  (SELECT port.id 
   FROM entrance_halls as port 
   ORDER BY senal.geom <#> port.geom LIMIT 1)
FROM  traffic_signs as senal;

MODIFICA 2 . Poiché questa domanda ha ricevuto molta attenzione e i vicini più vicini a k ​​(kNN) sono generalmente un problema difficile (in termini di runtime algoritmico) in GIS, sembra utile ampliare un po 'l'ambito originale di questa domanda.

Il modo standard per trovare i vicini x più vicini di un oggetto è usare un LATERAL JOIN (concettualmente simile a un per ogni loop). Prendendo in prestito spudoratamente dalla risposta di dbaston , faresti qualcosa del tipo:

SELECT
  signs.id,
  closest_port.id,
  closest_port.dist
 FROM traffic_signs
CROSS JOIN LATERAL 
  (SELECT
      id, 
      ST_Distance(ports.geom, signs.geom) as dist
      FROM ports
      ORDER BY signs.geom <-> ports.geom
     LIMIT 1
   ) AS closest_port

Quindi, se vuoi trovare le 10 porte più vicine, ordinate per distanza, devi semplicemente cambiare la clausola LIMIT nella sottoquery secondaria. Questo è molto più difficile da fare senza LATERAL JOINS e implica l'utilizzo della logica di tipo ARRAY. Mentre questo approccio funziona bene, può essere accelerato enormemente se sai che devi solo cercare a una data distanza. In questo caso, è possibile utilizzare ST_DWithin (signs.geom, doors.geom, 1000) nella sottoquery, che a causa del modo in cui l'indicizzazione funziona con l'operatore <-> - una delle geometrie dovrebbe essere una costante, piuttosto che un riferimento di colonna - potrebbe essere molto più veloce. Quindi, per esempio, per ottenere i 3 porti più vicini, entro 10 km, potresti scrivere qualcosa di simile al seguente.

 SELECT
  signs.id,
  closest_port.id,
  closest_port.dist
 FROM traffic_signs
CROSS JOIN LATERAL 
  (SELECT
      id, 
      ST_Distance(ports.geom, signs.geom) as dist
      FROM ports
      WHERE ST_DWithin(ports.geom, signs.geom, 10000)
      ORDER BY ST_Distance(ports.geom, signs.geom)
     LIMIT 3
   ) AS closest_port;

Come sempre, l'utilizzo varierà a seconda della distribuzione dei dati e delle query, quindi EXPLAIN è il tuo migliore amico.

Infine, c'è un gotcha minore, se si utilizza LEFT anziché CROSS JOIN LATERAL in quanto è necessario aggiungere ON TRUE dopo l'alias delle query laterali, ad es.

SELECT
  signs.id,
  closest_port.id,
  closest_port.dist
 FROM traffic_signs
LEFT JOIN LATERAL 
  (SELECT
      id, 
      ST_Distance(ports.geom, signs.geom) as dist
      FROM ports          
      ORDER BY signs.geom <-> ports.geom
      LIMIT 1
   ) AS closest_port
   ON TRUE;

Va notato che questo non funzionerà bene con grandi quantità di dati.
Jakub Kania,

@JakubKania. Dipende se puoi usare ST_DWithin o meno. Ma sì, punto preso. Sfortunatamente, l'ordinatore per operatore <-> / <#> richiede che una delle geometrie sia costante, no?
John Powell,

@ JohnPowellakaBarça hai qualche possibilità di sapere dove vive oggi quel post sul blog? - oppure una spiegazione simile degli operatori <-> e <#>? Grazie!!
DPSSpatial

@DPSSpatial, è fastidioso. Non lo so, ma c'è questo e questo che parla un po 'di questo approccio. Il secondo, utilizzando anche i giunti laterali, è un altro miglioramento interessante.
John Powell,

@DPSSpatial. È tutto un po 'scivoloso questo <->, <#> e roba di unione laterale. L'ho fatto con set di dati molto grandi e le prestazioni sono state orribili, senza usare ST_DWithin, che tutto ciò dovrebbe evitare. In definitiva, knn è un problema complicato, quindi l'utilizzo può variare. Buona fortuna :-)
John Powell,

13

Questo può essere fatto con un LATERAL JOINPostgreSQL 9.3+:

SELECT
  signs.id,
  closest_port.id,
  closest_port.dist
FROM traffic_signs
CROSS JOIN LATERAL 
  (SELECT
     id, 
     ST_Distance(ports.geom, signs.geom) as dist
     FROM ports
     ORDER BY signs.geom <-> ports.geom
   LIMIT 1) AS closest_port

10

L'approccio con cross-join non utilizza gli indici e richiede molta memoria. Quindi in pratica hai due scelte. Prima della 9.3 avresti usato una sottoquery correlata. 9.3+ puoi usare a LATERAL JOIN.

CONOSCI GIST con una svolta laterale Prossimamente in un database vicino a te

(domande esatte da seguire presto)


1
Fresco utilizzo di un giunto laterale. Non l'avevo mai visto prima in questo contesto.
John Powell,

1
@ JohnBarça È uno dei migliori contesti che abbia mai visto. Ho anche il sospetto che sarebbe utile quando hai davvero bisogno di usare ST_DISTANCE()per trovare il poligono più vicino e il cross join sta facendo esaurire la memoria del server. La query poligonale più vicina è ancora irrisolta AFAIK.
Jakub Kania,

2

@John Barça

ORDER BY è sbagliato!

ORDER BY senal.id, port.id, ST_Distance(port."GEOMETRY", senal."GEOMETRY");

Destra

senal.id, ST_Distance(port."GEOMETRY", senal."GEOMETRY"),port.id;

altrimenti restituirà non il più vicino, ma solo il piccolo ID porta


1
Quello corretto si presenta così (ho usato punti e linee):SELECT DISTINCT ON (points.id) points.id, lines.id, ST_Distance(lines.geom, points.geom) as dist FROM development.passed_entries As points, development."de_muc_rawSections_cleaned" As lines ORDER BY points.id, ST_Distance(lines.geom, points.geom),lines.id;
blackgis

1
OK, ora ti capisco. In realtà è probabilmente meglio usare l'approccio LATERAL JOIN, come nella risposta di @dbaston, che chiarisce quale cosa viene confrontata con quale altra cosa in termini di vicinanza. Non uso più l'approccio sopra.
John Powell,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.