A spatial join attaches the attributes of one GIS layer onto another layer based on where their features sit on the map, not on a shared ID field. Load both layers in QGIS or ArcGIS Pro, pick the spatial relationship you want, run the tool, then check the row counts before you trust the output.
A spatial join matches rows from a join layer to rows of a target layer based on a spatial relationship, such as points falling inside polygons, and writes the combined attributes into a new output feature class. It is the standard way to enrich coordinate-level records with area-level data when no common identifier exists.
Most beginner problems with spatial joins come down to three things: the layers are not in the same coordinate reference system, the two layers are in the wrong order, or the match option does not fit the geometry. Fix those first and the tool behaves predictably.
Table of Contents
- 1What You Need
- 2Step-by-Step: How to Do a Spatial Join in GIS
- 31. Decide What Should Match
- 42. Inspect and Prepare Both Layers
- 53. Run the Join in QGIS 3.x
- 64. Run the Join in ArcGIS Pro 3.x
- 75. Verify and Export the Result
- 8Common Mistakes
- 9Tips for Reproducible Spatial Joins
- 10Frequently Asked Questions
- 11What is a spatial join in GIS?
- 12Which spatial predicate should I use for a point-in-polygon join?
- 13What does the one-to-many spatial join option do?
- 14Why do my spatial join results contain more records than the target layer?
- 15Do the two layers need the same coordinate reference system before joining?
- 16Conclusion
What You Need
Two layers. One is the target layer, which keeps its own shape and record count. The other is the join layer, which donates its attributes.
A defined spatial relationship. Decide in plain language what “match” means for your data: a point sits inside a polygon, a line crosses a line, a parcel is closest to a river.
The same coordinate reference system on both layers. Not just “looks the same on screen” — the same CRS definition, or you reproject one layer onto the other first. Overlapping-looking layers can be a full continent apart if one is in geographic latitude and longitude and the other in a projected system in metres.
Suitable input formats work best as GeoPackage, file geodatabase, or GeoJSON. Shapefiles carry field name limits and encoding quirks that cause null attributes after a join, which is the single most common complaint in GIS forums. If you inherited .shp files, convert first.
And a tool. You have options outside the two desktop platforms:
| Tool | What it does | Pick it when |
|---|---|---|
| Spatial Join (ArcGIS Pro) | Writes a new feature class with joined attributes | You want the original layer untouched and an auditable output |
| Summarize Within (ArcGIS Pro 3.0 and later) | Aggregates join features into one summary row per target feature | You only need counts, sums or means per polygon |
| Add Spatial Join (ArcGIS Pro) | Adds the join fields directly to the target layer | You want a permanent field join on the layer itself |
| Join attributes by location (QGIS) | Creates a new layer with matched attributes | You are working in QGIS 3.x |
| Join Field (QGIS) | Table join on a shared key | Both layers already carry the same ID field |
For most beginners, the plain Spatial Join or Join attributes by location tools are the right starting point. Summarize Within is simpler once you only need aggregate numbers, and it avoids the duplicate-row problem described later.
Step-by-Step: How to Do a Spatial Join in GIS
1. Decide What Should Match
Ask one question first: which layer keeps its rows? The answer is your target layer, and the output geometry and feature count come from it. The other layer is the join layer, and its attributes are copied across.
A worked example: you have a point layer of school locations and a polygon layer of school districts with a median household income field. The schools are the target. Each school should receive the income value of the district containing it, so the output stays a point layer with one row per school.
Swap them and you get districts as the target, which means a one-to-many join that produces one row per school per district instead. That single decision explains most duplicate-row confusion.
The match option is the spatial relationship you are testing. Feature type limits which options even work:
| Target geometry | Join geometry | Usable relationships | Typical question |
|---|---|---|---|
| Point | Polygon | Within, Contains, Intersect, Closest | Which district is this school in? |
| Point | Point | Closest, Within a distance | Which clinic is nearest? |
| Line | Polygon | Intersect, Within, Contains | Which zone does this road cross? |
| Line | Line | Intersect, Closest | Where do the streams meet? |
| Polygon | Point | Contains, Within, Intersect | How many permits fall in this parcel? |
| Polygon | Line | Contains, Intersect | Which roads run through this flood zone? |
| Polygon | Polygon | Intersect, Contains, Within, Largest overlap | Which zones overlap? |
Note the direction. A point-in-polygon question asked the other way round is a polygon-within-polygon test, which almost never matches. Write your intended relationship as a sentence with a subject and a verb before you open the dialog.
2. Inspect and Prepare Both Layers

This is the step tutorials usually skip, and it is where the failures start. Run through it once and most of the forum threads about empty output stop applying to you.
Load both layers and open the attribute table for each. Note the feature count, the geometry type in the layer properties, and the fields you plan to bring over.
Check that both layers report the same coordinate reference system by name, not by appearance. Turn on a spatial reference widget if your software has one, or read the CRS field directly. If they differ, reproject the join layer to match the target layer’s CRS rather than setting a “defined” CRS by hand.
Look for empty key fields. A null or blank field on the target layer will never receive a value from a join, and it is easy to miss in a table with 40 columns.
Remove exact duplicate features if they are genuinely duplicates, because a feature that appears three times will contribute three copies of its attributes.
Repair invalid geometry before joining. Self-intersecting polygons and unclosed rings fail the intersection test silently, so those features simply never match. Every desktop GIS has a validity check in the processing toolbox.
Last, copy your source data into a working folder and do the join on the copy. A tool that mutates the target layer in place has no undo across sessions.
3. Run the Join in QGIS 3.x
In QGIS 3.x, open Processing and search for Join attributes by location, also reachable from the Vector menu under Data Management Tools. In older 2.x layouts this is a plugin rather than a core tool, so if you cannot find it your QGIS version is old enough to upgrade.
Set the input target layer to the layer whose rows you want to keep. Set the join layer to the layer donating attributes. Both fields are dropdowns populated from what you currently have loaded.
Choose the predicate. Within tests whether the target feature sits inside the join feature, which is the right choice for points into districts. Contains is the mirror image. Intersect is looser and will match any overlap at all.
Tick the fields you want to copy in the fields to copy list, or copy all and delete what you do not need afterwards. Leave the join field name prefix blank unless you have a name collision.
Choose the output format. GeoPackage (.gpkg) keeps geometry and attributes in one file and handles field names without the 10-character truncation you get with shapefiles. GeoJSON works well for web maps. CSV is fine only when you genuinely want attributes with no geometry.
Run it, then add the result layer to the canvas so you can see whether the points landed inside the expected polygons.
4. Run the Join in ArcGIS Pro 3.x

Open the Analysis tab, then Tools, then Spatial Join. The geoprocessing pane opens on the right with all parameters listed top to bottom in the order they run.
Set Target Features and Join Features to your two layers. Pick Match Option from the dropdown, which groups options by geometry combination and grey out the ones that do not apply to your layer types.
Choose the Join Operation. JOIN_ONE_TO_ONE keeps one row per target feature and aggregates matching join values using the field map merge rules. JOIN_ONE_TO_MANY creates a new row for every match, which is what you want when each match matters individually.
Set Keep All Target Features to keep every row even when nothing matched, giving you null values in the joined fields. Turn it off and you get an inner join that silently drops unmatched features.
In the Field Map, right a field to open its join properties and pick a merge rule. First, Last, Concatenate, Count, Sum, Mean, Min, Max and Median all behave differently when five points fall inside one polygon.
Leave Search Radius blank unless you are using a Closest or Within a distance option, where it sets the maximum matching distance in the units of the data’s coordinate system.
On ArcGIS Online, the web map editor offers Join under the Edit menu on a layer. It does the same matching against hosted feature services and writes the joined fields back to the layer, with fewer options than the desktop tool.
5. Verify and Export the Result
Verification takes about two minutes and catches nearly everything. Compare the output feature count against your target layer count. Equal means one-to-one ran as expected and nothing was duplicated. Larger means one-to-many and each target feature matched several join features. Smaller means unmatched features were dropped.
Look at the fields the tool adds:
| Field | What the value means | How to read it |
|---|---|---|
| Join_Count | Number of join features matched to that target feature | 0 means nothing matched; 1 means a clean one-to-one result |
| TARGET_FID | Object ID of the target feature | Use it to trace a row back to the original layer |
| JOIN_FID | Object ID of the matched join feature | A value of -1 means no join feature matched |
| Distance | Distance to the match, when a distance option was used | Units follow the data, not the display |
Filter Join_Count to 0 to see which features fell outside every polygon. In real data that is usually a handful of points in the ocean or a coordinate typo, and it is worth eyeballing them rather than deleting blindly.
Spot-check one record you can verify by hand. Pick a school you know sits inside a specific district, confirm the district name in the output, and you have proof the join direction and predicate are right.
Sort or summarise Join_Count and check for suspicious totals. A sum that is far larger than the number of join features means features are being counted once per polygon they touch, which is a double-counting problem rather than a tool fault.
Export with geometry intact: GeoPackage for analysis work, GeoJSON for web maps, a file geodatabase for ArcGIS teams. If someone asks for a CSV, remember you are handing them attributes only, and the spatial relationship is gone.
The same join in Python, for repeatable workflows:
import geopandas as gpd
schools = gpd.read_file("schools.gpkg")
districts = gpd.read_file("districts.gpkg")
joined = gpd.sjoin(
schools,
districts[["district_id", "median_income", "geometry"]],
how="left",
predicate="within"
)
joined.to_file("schools_with_districts.gpkg", layer="enriched")
The arcpy equivalent takes the match option and join operation as named parameters, with the field mappings controlling the merge rules.
Common Mistakes
Wrong layer order. If you expected districts to receive school attributes and got the opposite, the roles are reversed. The target layer decides the output geometry and row count, so read the field names in your output to confirm which one you chose.
Incompatible spatial references. Two layers in different CRSs may overlay correctly at low zoom and still produce zero matches at full extent, or the opposite. Reproject one layer to the other’s CRS before joining rather than editing the CRS definition by hand.
Wrong predicate. Within and Contains are not interchangeable, and Intersect is looser than most people expect, matching any overlap including a polygon that only clips the corner of another. Choose the relationship that matches your sentence.
Unexpected duplicates. One-to-many multiplies rows by the number of matches. Three points inside one district under a one-to-many join with the polygon as target gives three rows. Switch to a one-to-one operation or use Summarize Within.
Lost unmatched features. An inner join drops target features with no match. Turn on the keep-all option if the missing rows are meaningful, such as points outside any flood zone.
Confusing a table join with a spatial join. A Join Field on a shared ID needs no geometry relationship, and neither layer has to overlap the other. If both layers carry a parcel number, that is faster and more accurate.
Exporting a CSV and expecting polygons. A CSV has no geometry. Anything that depends on shape breaks immediately, so keep a GeoPackage or GeoJSON alongside it.
Features sitting exactly on a boundary. Software applies different rules about whether a shared edge counts as inside, which depends on the geometry model. Treat boundary cases as a known edge condition and check them by hand rather than trusting the count.
Invalid geometry. Self-intersecting or unclosed polygons fail silently and never match. Run a validity check and fix the flagged features first.
Tips for Reproducible Spatial Joins
Keep your originals untouched and join into a new output every time, so a wrong predicate costs you a rerun rather than a redownload.
Write down the software version, the match option, the join operation and the field names in your project notes. Six months later those four details explain any output file.
Test on a subset first. Fifty features will tell you in seconds whether the predicate is right, before you wait on a full county.
Use a stable unique ID column rather than relying on row order, so the result can be checked against the source later.
Compare CRS definitions rather than map appearance, and save the project file alongside the output data so the parameters travel with it.
Frequently Asked Questions
What is a spatial join in GIS?
A spatial join matches the rows of one GIS layer to the rows of another layer using their relative location on the map rather than a shared ID field. Each target feature is tested against the join features with a chosen spatial relationship such as Within or Intersect, and the matching attributes are written into a new output feature class. It is how you attach district income to schools or count permits per parcel when no common identifier exists.
Which spatial predicate should I use for a point-in-polygon join?
Use Within when the target features are points and the join features are polygons, which tests whether each point falls inside a polygon. Use Contains for the reverse direction. Intersect is looser and matches any overlap, which pulls in features that only touch an edge. In QGIS these appear as predicates on the Join attributes by location dialog; in ArcGIS Pro they are match options on the Spatial Join tool.
What does the one-to-many spatial join option do?
One-to-many keeps every match as its own row, so a target feature that intersects three join features produces three output rows carrying each matched record. One-to-one keeps a single row per target feature and collapses the matching values with merge rules such as First, Sum or Mean in the field map. Choose one-to-many when each individual match matters, and one-to-one when you only need a summary per target feature.
Why do my spatial join results contain more records than the target layer?
Your output has one row per match, not per target feature, which is what a one-to-many join operation does. Five permits inside one flood zone polygon become five rows when the polygon is the target. Check the join operation setting, and look at the Join_Count field, which stores how many join features matched each target feature. Switch to one-to-one if you only need a summary per polygon.
Do the two layers need the same coordinate reference system before joining?
Yes. Spatial relationships are computed from the coordinate values, so layers in different coordinate reference systems compare numbers that describe different places and the output is wrong or empty. Reproject one layer to match the other before you run the join, using the reproject tool in QGIS or the Project tool in ArcGIS Pro. Compare the CRS definition in layer properties rather than trusting how the layers line up on screen.
Conclusion
Start by opening both layers and confirming their CRS definitions match. Decide which layer keeps its rows, write the matching relationship as a sentence, and run the tool on a small subset first.
Then check the output feature count against your target layer, filter Join_Count for zeros, and spot-check one record you already know. Once those three checks pass, export the result with its geometry intact and note the parameters alongside the file.


