Вы почти там. Есть небольшая хитрость, которая заключается в использовании отдельного оператора 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 сравнения. Есть хорошая статья в блоге, объясняющая, как они работают. Общее, на что следует обратить внимание, это то, что эти два оператора работают в предложении 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;
РЕДАКТИРОВАТЬ 2 . Поскольку этому вопросу уделяется много внимания, и k-ближайшие соседи (kNN), как правило, представляют собой сложную проблему (с точки зрения алгоритмического времени выполнения) в ГИС, представляется целесообразным несколько расширить исходную область этого вопроса.
Стандартный способ найти x ближайших соседей одного объекта - использовать LATERAL JOIN (концептуально аналогично a для каждого цикла). Заимствуя бесстыдно из ответа dbaston , вы бы сделали что-то вроде:
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 в боковом подзапросе. Это гораздо сложнее обойтись без LATERAL JOINS и включает в себя использование логики типа ARRAY. Хотя этот подход работает хорошо, его можно значительно ускорить, если вы знаете, что вам нужно искать только на определенном расстоянии. В этом случае вы можете использовать ST_DWithin (signs.geom, ports.geom, 1000) в подзапросе, который из-за способа индексации работает с оператором <-> - одна из геометрий должна быть константой, а не ссылка на столбец - может быть намного быстрее. Так, например, чтобы получить 3 ближайших порта, в пределах 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 - ваш лучший друг.
Наконец, есть небольшая ошибка, если вы используете LEFT вместо CROSS JOIN LATERAL в том смысле, что вы должны добавить ON TRUE после псевдонима боковых запросов, например,
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;