Planetary-scale answers, unlocked.
A Hands-On Guide for Working with Large-Scale Spatial Data. Learn more.
A spatial join combines two tables by where their geometries sit. It pairs rows whose shapes meet a spatial condition, with no shared ID column needed: a point that falls inside a polygon, two parcels that share a boundary, a facility within 2 kilometers of a river. Most data about the physical world lacks a common key across sources, which makes the spatial join the operation that connects building footprints to flood zones, delivery stops to sales territories, and satellite pixels to farm fields.
ST_Intersects
ST_Contains
ST_DWithin
ST_Touches
Start from a query. This one counts the points of interest inside each Colorado city and town, using open data from the Overture Maps Foundation:
WITH localities AS ( SELECT id, names.primary AS city, geometry FROM wherobots_open_data.overture_maps_foundation.divisions_division_area WHERE subtype = 'locality' AND region = 'US-CO' ) SELECT l.id, l.city, COUNT(p.id) AS places FROM localities AS l JOIN wherobots_open_data.overture_maps_foundation.places_place AS p ON ST_Intersects(l.geometry, p.geometry) GROUP BY l.id, l.city
Three things make that JOIN spatial.
JOIN
A geometry or geography column on each side. A geometry column stores points, lines, or polygons as coordinates on a plane. A geography column stores them on the curved surface of the Earth, so distances come back in meters. A latitude and longitude held in two number columns becomes a geometry only after a constructor such as ST_Point builds it.
ST_Point
The same coordinate reference system. A CRS defines how coordinates map to places on the Earth. EPSG:4326 stores longitude and latitude in degrees. EPSG:2263 stores feet on the New York Long Island State Plane. Comparing geometries from two different systems returns rows, and those rows are wrong. ST_Transform converts one side to match the other.
ST_Transform
A spatial predicate. The predicate in the ON clause decides which pairs match. Each left row can match zero, one, or many right rows, so a point on a shared city line can appear twice in the output.
ON
Predicates fall into two groups. Topological predicates test how two shapes relate in space. Distance predicates test how far apart they are.
ST_Intersects(A, B)
ST_Contains(A, B)
ST_Within(B, A)
ST_Covers(A, B)
ST_Touches(A, B)
ST_Crosses(A, B)
ST_DWithin(A, B, d)
d
ST_Intersects runs in more production joins than any other predicate. PostGIS defines it as two geometries having any point in common, which makes it the broadest topological test: it matches a point inside a polygon, a point on the polygon's edge, a line that crosses a boundary, and two polygons that overlap. It is also symmetric, so ST_Intersects(A, B) and ST_Intersects(B, A) return the same rows, and no argument order can be wrong.
ST_Intersects(B, A)
ST_Covers exists because of a quirk in ST_Contains. Under the PostGIS definition, a polygon does not contain a point that sits exactly on its boundary, because the interiors share no point. ST_Covers only asks that no point of B lie outside A, so the boundary point matches.
ST_Covers
The join type decides what happens to rows that find no match.
COUNT(p.id)
On Overture data, the inner join above returns Colorado localities that hold at least one place. Switching JOIN to LEFT JOIN returns all 304 localities, including the 11 with no places at all. Those 11 rows are often the finding: towns with no mapped businesses, territories with no customers, zones with no sensors. The filter on localities sits inside the WITH clause, before the join, so it never removes the unmatched rows.
LEFT JOIN
WITH
A full outer join needs both sides filtered before the join, and it needs an ID from each side in the output, because an unmatched place has no city to group by:
-- Boulder area WITH localities AS ( SELECT id, geometry FROM wherobots_open_data.overture_maps_foundation.divisions_division_area WHERE subtype = 'locality' AND region = 'US-CO' AND bbox.xmin BETWEEN -105.35 AND -105.0 AND bbox.ymin BETWEEN 39.9 AND 40.1 ), places AS ( SELECT id, geometry FROM wherobots_open_data.overture_maps_foundation.places_place WHERE bbox.xmin BETWEEN -105.3 AND -105.2 AND bbox.ymin BETWEEN 39.98 AND 40.06 ) SELECT l.id AS locality_id, p.id AS place_id FROM localities l FULL OUTER JOIN places p ON ST_Intersects(l.geometry, p.geometry)
Rows with a null place_id are localities with no places. Rows with a null locality_id are places outside every locality. Around Boulder that comes to 11,914 matched pairs, 6 localities with no places, and 321 places outside every locality.
place_id
locality_id
A left join followed by WHERE p.id IS NULL is an anti join. It returns the rows with no spatial match, such as buildings outside every flood zone polygon, and it is the correct way to ask "what lies outside." ST_Disjoint in a join condition matches almost every pair and produces an enormous result.
WHERE p.id IS NULL
ST_Disjoint
In WherobotsDB, predicate joins, distance joins, and raster-vector joins all accept LEFT JOIN and FULL OUTER JOIN. The ST_KNN join is an inner join on the query table: a query row with no neighbor is left out.
FULL OUTER JOIN
ST_KNN
A predicate join, also called a range join, pairs rows whose geometries satisfy a topological predicate. Point in polygon is the standard case.
ON ST_Contains(zones.geometry, assets.geometry)
A distance join pairs rows whose geometries lie within a distance of each other.
ST_DWithin(a, b, 2)
useSpheroid = true
ON ST_DWithin(stations.geom_m, homes.geom_m, stations.walk_radius_m)
A nearest-neighbor join answers a different question: for each row on one side, which k rows on the other side are closest? It needs no radius and no containment.
ST_KNN(R, S, k, use_sphere, search_radius) takes five arguments:
ST_KNN(R, S, k, use_sphere, search_radius)
R
S
k
use_sphere
false
true
search_radius
Planar distance on EPSG:4326 data is measured in degrees, and a degree of longitude shrinks toward the poles, so the ranking can differ from ground distance. For nearest features in meters, transform both sides to a projected CRS for the area first. ST_AKNN takes the same arguments and returns approximate neighbors faster on large tables.
ST_AKNN
A raster stores a value in every cell of a grid: elevation, reflectance, land cover class. A raster-vector join pairs raster tiles with the vector geometries they overlap, using RS_Intersects, RS_Contains, or RS_Within, and then summarizes the pixels inside each geometry with zonal statistics such as RS_ZonalStats.
RS_Intersects
RS_Contains
RS_Within
RS_ZonalStats
mean
sum
count
customer_id
Attribute joins and spatial joins often run in the same pipeline. A spatial join assigns each building a parcel ID, and an attribute join on that ID then brings in the assessor's records.
The examples below run in a Wherobots notebook or SQL session against the open data catalog. Python cells start a Sedona session first.
from sedona.spark import SedonaContext config = SedonaContext.builder().getOrCreate() sedona = SedonaContext.create(config)
Count Overture places in every Colorado locality, keeping localities with none.
places_per_city = sedona.sql(""" WITH localities AS ( SELECT id, names.primary AS city, geometry FROM wherobots_open_data.overture_maps_foundation.divisions_division_area WHERE subtype = 'locality' AND region = 'US-CO' ), places AS ( -- Prefilter places to Colorado's bounding box before the join SELECT id, geometry FROM wherobots_open_data.overture_maps_foundation.places_place WHERE bbox.xmin BETWEEN -109.06 AND -102.04 AND bbox.ymin BETWEEN 36.99 AND 41.0 ) SELECT l.city, COUNT(p.id) AS place_count FROM localities l LEFT JOIN places p ON ST_Intersects(l.geometry, p.geometry) GROUP BY l.id, l.city ORDER BY place_count DESC """) places_per_city.show(10)
Find the nearest hospital to every school in a box around central Denver, which takes in parts of neighboring cities. Both sides move to UTM zone 13N (EPSG:26913), so planar distances come back in meters.
WITH schools AS ( SELECT id, names.primary AS school, ST_Transform(geometry, 'EPSG:4326', 'EPSG:26913') AS geom_m FROM wherobots_open_data.overture_maps_foundation.places_place WHERE taxonomy.primary IN ('elementary_school', 'middle_school', 'high_school') AND bbox.xmin BETWEEN -105.11 AND -104.6 AND bbox.ymin BETWEEN 39.61 AND 39.91 ), hospitals AS ( -- Search a wider area so schools near the edge still find their true nearest hospital SELECT id, names.primary AS hospital, ST_Transform(geometry, 'EPSG:4326', 'EPSG:26913') AS geom_m FROM wherobots_open_data.overture_maps_foundation.places_place WHERE taxonomy.primary = 'hospital' AND bbox.xmin BETWEEN -105.3 AND -104.4 AND bbox.ymin BETWEEN 39.4 AND 40.1 ) SELECT s.school, h.hospital, ROUND(ST_Distance(s.geom_m, h.geom_m)) AS distance_m FROM schools s JOIN hospitals h ON ST_KNN(s.geom_m, h.geom_m, 1, false) ORDER BY distance_m DESC
On Overture data the query pairs 596 schools in that box with a hospital. To report on one city, filter the schools with ST_Intersects against its boundary first. The average school sits 1,589 meters from its nearest hospital, and the farthest sits 9,915 meters away.
Compute the mean elevation of every Colorado city and town from the Copernicus 30 m DEM. A town can span several raster tiles, so the query sums pixel values and pixel counts per tile and divides once per town, which weights each tile by its share of the town.
WITH towns AS ( SELECT id, names.primary AS town, geometry FROM wherobots_open_data.overture_maps_foundation.divisions_division_area WHERE subtype = 'locality' AND region = 'US-CO' ), dem AS ( -- Keep only elevation tiles that touch Colorado SELECT rast FROM wherobots_open_data.copernicus_dem.glo_30m WHERE ST_Intersects(footprint, ST_GeomFromText( 'POLYGON((-109.1 36.9, -102 36.9, -102 41.1, -109.1 41.1, -109.1 36.9))')) ), per_tile AS ( SELECT t.id, t.town, RS_ZonalStats(d.rast, t.geometry, 1, 'sum') AS elev_sum, RS_ZonalStats(d.rast, t.geometry, 1, 'count') AS cells FROM towns t JOIN dem d ON RS_Intersects(d.rast, t.geometry) ) SELECT town, ROUND(SUM(elev_sum) / SUM(cells)) AS mean_elevation_m FROM per_tile GROUP BY id, town ORDER BY mean_elevation_m DESC
Alma comes out at a mean of 3,185 meters and Leadville at 3,088 meters. The same pattern summarizes canopy height, land cover, or any other raster inside parcels, fields, or flood zones.
An attribute join on a key can hash one side and probe it with the other, one pass over each table. A spatial predicate has no hash. Tested naively, every left row is compared with every right row: 10 million buildings against 50,000 flood zone polygons is 500 billion geometry tests, and each test walks coordinate arrays that can hold thousands of vertices for a detailed coastline or county boundary.
Every spatial engine attacks this the same way at a high level. It compares cheap bounding boxes first, discards pairs whose boxes cannot touch, and runs the exact predicate only on the candidates that remain. The engines differ in how well that pruning scales across many machines, how they handle skew, where a few dense cities hold most of the points, and how much tuning they demand from the user. A join that works on a sample in a single-node database such as PostGIS can stall on a full country.
WherobotsDB is the spatial engine in Wherobots, built by the original creators of Apache Sedona and compatible with Sedona's APIs. It plans spatial joins automatically: the docs describe spatial indexing as managed for you, without manual tuning. The same SQL runs on a sample and on a national table.
ST_Distance
use_sphere = true
ST_Contains(zone, asset)
ST_Contains(asset, zone)
subtype = 'county'
subtype = 'locality'
ST_IsValid
ST_MakeValid
Run your first spatial join on the Wherobots free tier at cloud.wherobots.com.
A spatial join combines two tables by a spatial relationship between their geometries, such as one shape containing, touching, or lying within a set distance of another. Each output row pairs a record from the left table with a record from the right table that meets the condition. A spatial join needs no shared ID column, because location does the matching.
A store point that falls inside a city boundary, a pipeline that crosses a river, two parcels that share a boundary, and a school within 500 meters of a highway are all spatial relationships. In SQL each one is a predicate: ST_Contains, ST_Crosses, ST_Touches, and ST_DWithin.
ST_Crosses
Use a spatial join when two datasets describe the same places but share no key. Common cases are assigning points to the polygon they fall in, counting features per zone, finding the nearest facility to each customer, and summarizing raster values such as elevation or land cover inside each polygon.
In ArcGIS Pro, run the Spatial Join tool in the Analysis toolbox. Set the target features and the join features, choose a match option such as Intersect or Within a distance, and choose Join one to one or Join one to many. The tool writes the target features and the joined attributes to a new feature class.
An attribute join matches rows on equal values in a shared column, such as a parcel ID or a ZIP code. A spatial join matches rows on a spatial predicate between two geometry columns. Use an attribute join when a reliable key exists, and a spatial join when the only link between the datasets is location.
Yes. An inner spatial join keeps only pairs that match. A left spatial join keeps every row of the left table and fills the right columns with nulls where nothing matches, and a full outer join keeps unmatched rows from both sides. In WherobotsDB, predicate joins, distance joins, and raster-vector joins take LEFT JOIN and FULL OUTER JOIN in SQL. The ST_KNN nearest-neighbor join is an inner join.
With GeoPandas, call geopandas.sjoin with a predicate such as intersects or within and a how of inner, left, or right. GeoPandas runs on one machine. For tables too large for memory, run the same join in PySpark with WherobotsDB, using ST_Intersects or another predicate in the join condition.
predicate
how
A KNN join returns, for each row in one table, the k closest rows in another table. In WherobotsDB, ST_KNN(queries, objects, k, use_sphere) takes the query geometry, the object geometry, the number of neighbors, and a distance model, with an optional search radius. ST_AKNN returns approximate neighbors faster on large tables.
ST_KNN(queries, objects, k, use_sphere)
What is Sentinel-2? Bands, Resolution, Data Access, Uses
Sentinel-2 is the Copernicus optical satellite mission that images Earth's land and coasts in 13 spectral bands at 10 to 60 m every five days. Its bands, resolution, products, free data access, and uses.
What is Synthetic Aperture Radar (SAR)? Bands, Data, Uses
Synthetic aperture radar (SAR) images the ground with microwaves, day or night, through cloud. How SAR works, its bands, products, and data sources.
What is Stochastic Modeling? Definition, Types, Examples
Stochastic modeling simulates thousands of possible outcomes from random variables. How it works, stochastic vs deterministic, and how cat models use it.
share this article
Awesome that you’d like to share our articles. Where would you like to share it to: