Найближчі точки PostGIS з ST_Distance, kNN


23

Мені потрібно отримати на кожному елементі однієї таблиці найближчу точку іншої таблиці. Перша таблиця містить дорожні знаки, а друга - вхідні зали міста. Річ у тім, що я не можу використовувати функцію ST_ClosestPoint, і мені потрібно використовувати функцію ST_Distance і отримувати запис min (ST_distance), але я сильно застряг, будуючи запит.

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

Мені потрібно отримати ідентифікатор найближчого entrnce_hall кожного promet_sign.

Поки що мій запит:

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

Завдяки цьому я отримую відстань від кожного транспортного знаку до кожного входу. Але як я можу отримати лише мінімальну відстань?

З повагою,


Яка версія PostgreSQL?
Якуб Каня

Відповіді:


41

Ви майже там. Існує невелика хитрість, що полягає у використанні окремого оператора Postgres , який поверне першу відповідність кожної комбінації - як ви замовляєте ST_Distance, це ефективно поверне найближчу точку від кожного сеналу до кожного порту.

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

Якщо ви знаєте, що мінімальна відстань у кожному випадку не більше деякої суми x, (і у вас є просторовий індекс на ваших таблицях), ви можете прискорити це, поставивши WHERE ST_DWithin(port."GEOMETRY", senal."GEOMETRY", distance), наприклад, якщо, як відомо, всі мінімальні відстані не більше 10 км, то:

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

Очевидно, це потрібно використовувати обережно, так як якщо мінімальна відстань більша, ви просто не отримаєте рядок для комбінації сеналу та порту.

Примітка: Порядок за наказом повинен відповідати окремому за замовленням, що має сенс, оскільки окреме приймає першу окрему групу на основі певного впорядкування.

Передбачається, що у вас обох таблиць є просторовий індекс.

ЗРІД 1 . Існує ще один варіант, який полягає у використанні операторів <-> та <#> Postgres (обчислення центральної точки та граничної рамки відповідно), які дозволяють ефективніше використовувати просторовий індекс та не вимагають злому ST_DWithin, щоб уникнути n ^ 2 порівняння. Є хороша стаття в блозі, яка пояснює, як вони працюють. Загальне, що слід зазначити, ці два оператори працюють у пункті ЗАМОВЛЕННЯ ПО.

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 . Оскільки це питання привертає багато уваги, а k-найближчі сусіди (kNN), як правило, є складною проблемою (з точки зору алгоритмічного виконання) в ГІС, здається, варто трохи розширити вихідну сферу застосування цього питання.

Стандартний спосіб пошуку x найближчих сусідів одного об’єкта - це використання ЛІТЕРАЛЬНОГО ПРИЄДНАННЯ (концептуально подібного до кожного циклу). Безсоромно запозичивши відповідь дбастона , ви зробите щось на кшталт:

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 портів, упорядкованих за відстані, вам просто потрібно змінити пункт LIMIT у бічному підзапиті. Це набагато складніше обійтися без ЛІТЕРАЛЬНИХ ПРИЄДНАНЬ та передбачає використання логіки типу ARRAY. Незважаючи на те, що цей підхід працює добре, він може бути надзвичайно прискорений, якщо ви знаєте, що вам потрібно лише шукати на певну відстань. У цьому випадку ви можете використовувати ST_DWithin (знаки.geom, ports.geom, 1000) у підзапиті, який через спосіб індексації працює з оператором <-> - одна з геометрій повинна бути константою, а не посилання на стовпчик - може бути набагато швидшим. Так, наприклад, щоб отримати три найближчих порти, в межах 10 км, ви можете написати щось подібне.

 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;

Як завжди, використання буде змінюватися залежно від вашого розповсюдження даних та запитів, тому EXPLAIN - ваш найкращий друг.

Нарешті, є незначна приналежність, якщо використовувати лівий замість CROSS JOIN LATERAL, що вам слід додати НАДАЛЬНО після псевдоніму бічних запитів, наприклад,

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;

Слід зазначити, що з великою кількістю даних це не буде добре.
Якуб Каня

@JakubKania. Це залежить від того, можна використовувати ST_DWithin чи ні. Але, так, точка. На жаль, оператор Порядок від <-> / <#> вимагає, щоб одна з геометрій була постійною, ні?
Джон Пауелл

@ JohnPowellakaBarça Ви хочете дізнатися, де живе ця публікація щоденника? - або подібне пояснення операторів <-> і <#>? Спасибі!!
DPSSpatial

@DPSSpatial, це дратує. Я не знаю, але є це і це, що трохи говорить про такий підхід. 2-й, також використовуючи бічні з'єднання, що є ще одним цікавим доповненням.
Джон Пауелл

@DPSSpatial. Це все трохи слизьке це <->, <#> та бічне з'єднання. Я робив це з дуже великими наборами даних, і продуктивність була жахливою, не використовуючи ST_DWithin, чого всього цього слід уникати. Зрештою, knn є складною проблемою, тому використання може відрізнятися. Удачі :-)
Джон Пауелл

13

Це можна зробити за допомогою 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

Підступ з перехресним приєднанням не використовує індексів і вимагає багато пам'яті. Отже, у вас є два варіанти. Попередньо 9.3 ви б використовували співвіднесений запит. 9.3+ ви можете використовувати LATERAL JOIN.

KNN GIST з бічним поворотом Незабаром до бази даних поблизу вас

(точні запити, які слід швидко виконати)


1
Класне використання бічного з'єднання. Я цього раніше не бачив у цьому контексті.
Джон Пауелл

1
@ JohnBarça Це один з найкращих контекстів, які я бачив. Я також підозрюю, що це буде корисно, коли вам дійсно потрібно скористатися, ST_DISTANCE()щоб знайти найближчий багатокутник, а перехресне з'єднання спричиняє, що у сервера не вистачає пам'яті. Найближчий запит на багатокутник досі залишається невирішеним AFAIK.
Якуб Каня

2

@John Barça

ЗАМОВЛЕННЯ неправильно!

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

Правильно

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

інакше він повернеться не найближчим, лише той, який має маленький ідентифікатор порту


1
Правильний виглядає так (я використовував точки і лінії):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
Гаразд, я зараз тебе отримую. Насправді, мабуть, краще використовувати підхід ЛІТЕРАЛЬНОГО ПРИЄДНАННЯ, як у відповіді @ dbaston, який дає зрозуміти, яка річ порівняна з тією, що стосується близькості. Я вже не використовую підхід вище.
Джон Пауелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.