PostGIS poin terdekat dengan ST_Distance, kNN

23

Saya perlu mendapatkan pada setiap elemen pada satu tabel titik terdekat dari tabel lain. Tabel pertama berisi rambu lalu lintas dan yang kedua adalah Aula Masuk kota. Masalahnya adalah bahwa saya tidak dapat menggunakan fungsi ST_ClosestPoint dan saya harus menggunakan fungsi ST_Distance dan mendapatkan catatan min (ST_distance) tetapi saya cukup buntu dalam membangun kueri.

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

Saya perlu mendapatkan id dari entrnce_hall terdekat dari setiap traffic_sign.

Permintaan saya sejauh ini:

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

Dengan ini saya mendapatkan jarak dari setiap traffic_sign ke setiap entrance_hall. Tetapi bagaimana saya bisa mendapatkan jarak minimun saja?

Salam,

Egidi
sumber
Versi PostgreSQL apa?
Jakub Kania

Jawaban:

41

Anda hampir sampai. Ada sedikit trik untuk menggunakan operator berbeda Postgres , yang akan mengembalikan kecocokan pertama dari setiap kombinasi - karena Anda memesan oleh ST_Distance, secara efektif itu akan mengembalikan titik terdekat dari setiap senal ke setiap port.

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

Jika Anda tahu bahwa jarak minimum dalam setiap kasus tidak lebih dari jumlah x, (dan Anda memiliki indeks spasial pada tabel Anda), Anda dapat mempercepat ini dengan meletakkan WHERE ST_DWithin(port."GEOMETRY", senal."GEOMETRY", distance), misalnya, jika semua jarak minumum diketahui tidak lebih dari 10 km, maka:

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

Jelas, ini perlu digunakan dengan hati-hati, karena jika jarak minimum lebih besar, Anda tidak akan mendapatkan baris untuk kombinasi senal dan port.

Catatan: Pesanan demi pesanan harus sesuai dengan pesanan berbeda, yang masuk akal, karena berbeda adalah mengambil grup berbeda pertama berdasarkan beberapa pemesanan.

Diasumsikan bahwa Anda memiliki indeks spasial di kedua tabel.

EDIT 1 . Ada opsi lain, yaitu menggunakan operator <-> dan <#> Postgres, (masing-masing, perhitungan titik pusat dan kotak batas) yang menggunakan indeks spasial secara lebih efisien dan tidak memerlukan peretasan ST_DWithin untuk menghindari n ^ 2 perbandingan. Ada artikel blog bagus yang menjelaskan cara kerjanya. Hal umum yang perlu diperhatikan adalah bahwa kedua operator ini bekerja di klausa 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;

EDIT 2 . Karena pertanyaan ini telah menerima banyak perhatian dan k-tetangga terdekat (kNN) umumnya merupakan masalah yang sulit (dalam hal run-time algoritmik) di GIS, tampaknya ada gunanya untuk memperluas sedikit pada lingkup asli dari pertanyaan ini.

Cara standar untuk menemukan x tetangga terdekat dari satu objek adalah dengan menggunakan LATERAL JOIN (secara konseptual mirip dengan a untuk setiap loop). Meminjam tanpa malu dari jawaban dbaston , Anda akan melakukan sesuatu seperti:

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

Jadi, jika Anda ingin menemukan 10 port terdekat, dipesan berdasarkan jarak, Anda cukup mengubah klausa LIMIT di sub-kueri lateral. Ini jauh lebih sulit untuk dilakukan tanpa GABUNGAN LATERAL dan melibatkan penggunaan logika tipe ARRAY. Meskipun pendekatan ini bekerja dengan baik, itu dapat dipercepat jika Anda tahu Anda hanya perlu mencari jarak yang diberikan. Dalam contoh ini, Anda dapat menggunakan ST_DWithin (signs.geom, ports.geom, 1000) dalam subquery, yang karena cara pengindeksan bekerja dengan operator <-> - salah satu geometri harus berupa konstanta, bukan sebuah referensi kolom - mungkin jauh lebih cepat. Jadi, misalnya, untuk mendapatkan 3 pelabuhan terdekat, dalam jarak 10 km, Anda dapat menulis sesuatu seperti berikut ini.

 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;

Seperti biasa, penggunaan akan bervariasi tergantung pada distribusi dan kueri data Anda, jadi EXPLAIN adalah teman terbaik Anda.

Akhirnya, ada gotcha minor, jika menggunakan LEFT daripada CROSS JOIN LATERAL , Anda harus menambahkan ON TRUE setelah permintaan lateral alias, misalnya,

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;
John Powell
sumber
Perlu dicatat bahwa ini tidak akan bekerja dengan baik dengan sejumlah besar data.
Jakub Kania
@ JakubKania. Itu tergantung pada apakah Anda dapat menggunakan ST_DWithin atau tidak. Tapi, ya, poin diambil. Sayangnya, operator Order oleh <-> / <#> membutuhkan salah satu geometri menjadi konstan, bukan?
John Powell
@ JohnPowellakaBarça setiap kesempatan Anda tahu di mana posting blog tinggal saat ini? - atau, penjelasan serupa dari operator <-> dan <#>? Terima kasih!!
DPSSpatial
@ DPSSpatial, itu menyebalkan. Saya tidak, tetapi ada ini dan ini yang berbicara sedikit tentang pendekatan ini. Yang kedua, menggunakan gabungan lateral juga, yang merupakan perangkat tambahan menarik lainnya.
John Powell
@DPSSpatial. Itu semua agak licin ini <->, <#> dan hal-hal bergabung lateral. Saya telah melakukan ini dengan dataset yang sangat besar dan kinerjanya mengerikan, tanpa menggunakan ST_DWithin, yang seharusnya dihindari semua ini. Pada akhirnya, knn adalah masalah yang rumit, jadi penggunaannya bisa beragam. Semoga berhasil :-)
John Powell
13

Ini dapat dilakukan dengan LATERAL JOINdi PostgreSQL 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
dbaston
sumber
10

Pendekatan dengan cross-join tidak menggunakan indeks dan membutuhkan banyak memori. Jadi pada dasarnya Anda punya dua pilihan. Pre 9.3 Anda akan menggunakan subquery yang berhubungan. 9.3+ Anda dapat menggunakan a LATERAL JOIN.

KNN GIST dengan twist Lateral Segera hadir ke database di dekat Anda

(pertanyaan pasti akan segera menyusul)

Jakub Kania
sumber
1
Penggunaan sambungan lateral yang keren. Belum pernah melihat itu sebelumnya dalam konteks ini.
John Powell
1
@ JohnBarça Ini salah satu konteks terbaik yang pernah saya lihat. Saya juga curiga akan sangat membantu ketika Anda benar-benar perlu menggunakan ST_DISTANCE()untuk menemukan poligon terdekat dan cross join menyebabkan server kehabisan memori. Kueri poligon terdekat masih AFAIK yang belum terpecahkan.
Jakub Kania
2

@ John Barça

ORDER BY salah!

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

Kanan

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

jika tidak, ia tidak akan mengembalikan yang terdekat, hanya yang memiliki sedikit port id

strech
sumber
1
Yang benar terlihat seperti ini (saya menggunakan poin dan garis):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
Baik, saya mengerti sekarang. Sebenarnya mungkin lebih baik menggunakan pendekatan GABUNGAN LATERAL, seperti pada jawaban @ dbaston, yang memperjelas hal apa yang dibandingkan dengan hal lain dalam hal kedekatan. Saya tidak menggunakan pendekatan di atas lagi.
John Powell