- Automatically optimizes range join query and distance join query.
- Automatically performs predicate pushdown.
Range join
Introduction: Find geometries from A and geometries from B such that each geometry pair satisfies a certain predicate. Most predicates supported by Spatial SQL can trigger a range join. SQL example:Distance join
Introduction: Find geometries from A and geometries from B such that the distance of each geometry pair is less or equal than a certain distance. It supports the planar Euclidean distance calculatorsST_Distance, ST_HausdorffDistance, ST_FrechetDistance and the meter-based geodesic distance calculators ST_DistanceSpheroid and ST_DistanceSphere.
SQL example for planar Euclidean distance:
Only consider
ST_DistanceSpheroid (works for ST_DistanceSphere too):
Less than a certain distance==
Broadcast index join
Introduction: Perform a range join or distance join but broadcast one of the sides of the join. This maintains the partitioning of the non-broadcast side and doesn’t require a shuffle. Sedona will create a spatial index on the broadcasted table. Sedona uses broadcast join only if the correct side has a broadcast hint. The supported join type - broadcast side combinations are:- Inner - either side, preferring to broadcast left if both sides have the hint
- Left semi - broadcast right
- Left anti - broadcast right
- Left outer - broadcast right
- Right outer - broadcast left
ST_Distance, ST_DistanceSpheroid, ST_DistanceSphere, ST_HausdorffDistance or ST_FrechetDistance:
pointDf1 above).
Automatic broadcast index join
When one table involved a spatial join query is smaller than a threshold, Sedona will automatically choose broadcast index join instead of Sedona optimized join. The current threshold is controlled by sedona.join.autoBroadcastJoinThreshold and set to the same asspark.sql.autoBroadcastJoinThreshold.
Raster join
The optimization for spatial join also works for raster predicates, such asRS_Intersects, RS_Contains and RS_Within.
SQL Example:
Google S2 based approximate equi-join
If the performance of Sedona optimized join is not ideal, which is possibly caused by complicated and overlapping geometries, you can resort to Sedona built-in Google S2-based approximate equi-join. This equi-join leverages Spark’s internal equi-join algorithm and might be performant given that you can opt to skip the refinement step by sacrificing query accuracy. Please use the following steps:1. Generate S2 ids for both tables
Use ST_S2CellIds to generate cell IDs. Each geometry may produce one or more IDs.2. Perform equi-join
Join the two tables by their S2 cellId3. Optional: Refine the result
Due to the nature of S2 Cellid, the equi-join results might have a few false-positives depending on the S2 level you choose. A smaller level indicates bigger cells, less exploded rows, but more false positives. To ensure the correctness, you can use one of the Spatial Predicates to filter out them. Use this query instead of the query in Step 2.ST_Contains, to remove false positives. You can also use ST_Intersects and so on.
4. Optional: De-duplicate
Due to the explode function used when we generate S2 Cell Ids, the resulting DataFrame may have several duplicate<lcs_geom, rcs_geom> matches. You can remove them by performing a GroupBy query.
first function is to take the first value from a number of duplicate values.
If you don’t have a unique id for each geometry, you can also group by geometry itself. See below:
If you are doing point-in-polygon join, this is not a problem and you can safely discard this issue. This issue only happens when you do polygon-polygon, polygon-linestring, linestring-linestring join.
S2 for distance join
This also works for distance join. You first need to useST_Buffer(geometry, distance) to wrap one of your original geometry column. If your original geometry column contains points, this ST_Buffer will make them become circles with a radius of distance.
Since the coordinates are in the longitude and latitude system, so the unit of distance should be degree instead of meter or mile. You can get an approximation by performing METER_DISTANCE/111000.0, then filter out false-positives. Note that this might lead to inaccurate results if your data is close to the poles or antimeridian.
In a nutshell, run this query first on the left table before Step 1. Please replace METER_DISTANCE with a meter distance. In Step 1, generate S2 IDs based on the buffered_geom column. Then run Step 2, 3, 4 on the original geom column.
Regular spatial predicate pushdown
Introduction: Given a join query and a predicate in the same WHERE clause, first executes the Predicate as a filter, then executes the join query. SQL example:Push spatial predicates to GeoParquet
Sedona supports spatial predicate push-down for GeoParquet files. When spatial filters were applied to dataframes backed by GeoParquet files, Sedona will use thebbox properties in the metadata
to determine if all data in the file will be discarded by the spatial predicate. This optimization could reduce the number of files scanned
when the queried GeoParquet dataset was partitioned by spatial proximity.
To maximize the performance of Sedona GeoParquet filter pushdown, we suggest that you sort the data by their geohash values (see ST_GeoHash) and then save as a GeoParquet file. An example is as follows:
bboxes of all GeoParquet files were plotted as blue rectangles and the query window was plotted as a red rectangle. Sedona will only scan 1 of the 6 files to
answer queries such as SELECT * FROM geoparquet_dataset WHERE ST_Intersects(geom, <query window>), thus only part of the data covered by the light green rectangle needs to be scanned.

Spatial predicate push-down to GeoParquet is enabled by default. Users can manually disable it by setting the Spark configuration
spark.sedona.geoparquet.spatialFilterPushDown to false.
Box2D filter pushdown
When a query filters on aBox2D-typed column (see Box2D Functions) using ST_Intersects or ST_Contains against a literal Box2D, Sedona translates the predicate into Parquet row-group inequalities on the column’s underlying xmin / ymin / xmax / ymax leaves and pushes them down via ParquetInputFormat.setFilterPredicate. Parquet’s row-group statistics machinery then skips row groups whose recorded min/max disprove the predicate — no file metadata scan is required.
The pushdown applies whenever the column’s Spark dataType is Box2DUDT. The simplest way to get one is to materialize the column with ST_Box2D(geom) before writing the dataset, or to use the SQL cast CAST(geom AS box2d). Sedona’s auto-generated <geom>_bbox covering column is written as a plain struct<xmin, ymin, xmax, ymax> — it satisfies the GeoParquet 1.1 covering-bbox contract but is not a Box2D, so ST_Intersects / ST_Contains do not target it directly. Use it through Push spatial predicates to GeoParquet (which prunes via the file-level bbox metadata), or write the column explicitly as ST_Box2D(geom) if you want row-group-level pruning through Box2D predicates.
Pushdown is enabled by default and is gated by two flags. The optimizer rule that attaches the Box2D spatial filter is controlled by
spark.sedona.geoparquet.spatialFilterPushDown (Sedona’s master spatial-pushdown toggle, default true); the actual injection into the Parquet read path is then additionally gated by Spark’s spark.sql.parquet.filterPushdown (default true). Disabling either disables Box2D pushdown.
Inverted-bound literals (xmin > xmax / ymin > ymax) are not pushed down — the predicate falls back to per-row evaluation so callers see the expected IllegalArgumentException from the scalar contract.
Box2D spatial join
ST_Intersects and ST_Contains between two Box2D columns route through the same physical operators as their Geometry counterparts (ST_Intersects / ST_Covers). At the executor boundary, each Box2D row is materialized into the implied rectangular polygon, after which the partitioner, R-tree index, and refine evaluator run unchanged. JTS short-circuits axis-aligned rectangle predicates via RectangleIntersects / RectangleContains, so the refine step pays only the four-double envelope comparison.
ST_Contains uses SpatialPredicate.COVERS semantics at the join layer — JTS covers matches ST_Contains’s closed-interval contract (JTS contains, strict-interior, would reject edge-sharing pairs).
Range join:
IllegalArgumentException from the join-side validation path, matching the scalar predicate contract.


