Short answer
ST_SetSRID and ST_Transform look similar and do opposite things. ST_SetSRID(geom, srid) changes only the label — it stamps a new SRID onto the geometry and leaves every X/Y coordinate exactly as it was. ST_Transform(geom, srid) changes the numbers — it reprojects the coordinates from their current CRS into the target CRS, physically moving the geometry. Use ST_SetSRID to declare a CRS that was missing or wrong; use ST_Transform to convert from one CRS to another. Swapping them is the single most common silent CRS bug in PostGIS, and it never throws an error — it just puts your data in the wrong place.
The mental model
Think of a geometry as a set of numbers plus a sticky note saying which coordinate system the numbers belong to.
ST_SetSRIDrewrites the sticky note. The numbers don't move. If you change the note from "SRID 0" to "4326" but the numbers were UTM eastings/northings, you have now lied about the data — it claims to be lon/lat degrees but holds metres.ST_Transformreads the note, looks up both the source and target CRS inspatial_ref_sys, runs the PROJ coordinate operation, and writes new numbers plus a new note. The geometry ends up in the right real-world position expressed in the new system.
Because ST_SetSRID only edits metadata, it is instant and lossless on the numbers. ST_Transform does real geodetic maths and requires both CRS definitions to exist.
When to use ST_SetSRID
Use it only when the coordinates are already correct for some CRS but the SRID is absent (0) or wrong:
-- Imported lon/lat data arrived as SRID 0; the numbers are genuinely WGS84
UPDATE observations
SET geom = ST_SetSRID(geom, 4326)
WHERE ST_SRID(geom) = 0;
Another legitimate case is building geometry from scratch, where you supply the SRID at construction:
SELECT ST_SetSRID(ST_MakePoint(2.349, 48.853), 4326); -- a point in Paris, WGS84
The test for whether ST_SetSRID is appropriate: do the existing numbers already mean the right thing in the CRS you're stamping? If yes, relabel. If no, you need to transform.
When to use ST_Transform
Use it whenever the coordinates must change systems — to align two layers, to work in metres, to publish in Web Mercator:
-- Convert British National Grid easting/northing to WGS84 lon/lat
SELECT ST_Transform(geom, 4326) FROM sites_bng;
For correct distance and area, transform geographic data into a projected CRS first, because lengths and areas computed in degrees are meaningless:
-- Buffer 500 metres around points stored in WGS84
SELECT ST_Buffer(ST_Transform(geom, 32631), 500) FROM wells;
Here the points go from EPSG:4326 (degrees) into EPSG:32631 (UTM 31N, metres), the 500 m buffer is built in metres, and you'd transform back to 4326 only for display.
The two functions used together
The idiomatic fix for "unlabelled data that also needs reprojecting" chains them. Set the true source SRID, then transform:
-- Numbers are UTM 31N metres but arrived as SRID 0; we want WGS84
SELECT ST_Transform(ST_SetSRID(geom, 32631), 4326)
FROM imported_layer;
Order matters: ST_SetSRID first establishes what the numbers are, then ST_Transform moves them to where you want. Reverse the order and ST_Transform would error (it cannot transform from SRID 0, an unknown source).
Worked example: diagnosing a misplaced layer
A colleague reports a points layer plotting in the Atlantic off Ghana — the null-island/equator symptom. Diagnose:
SELECT ST_SRID(geom), ST_Extent(geom) FROM badpoints LIMIT 1;
Result: srid = 4326, extent BOX(412300 5648200, 419800 5655100). The label says degrees (4326) but the numbers are in the hundreds of thousands — those are clearly projected metres (UTM), not lon/lat. So someone ran ST_SetSRID(geom, 4326) on UTM data, mislabelling it. The numbers never changed, so PostGIS interprets 412300 as 412300 degrees, wrapping the point to a nonsense location.
The fix is not another ST_SetSRID(…, 4326). First restore the truthful source label (UTM 31N), then transform to the CRS you actually want:
UPDATE badpoints
SET geom = ST_Transform(ST_SetSRID(geom, 32631), 4326);
Now the extent should read small lon/lat values and the points land in Ghana correctly.
Performance and indexing notes
ST_Transform is computed, so wrapping it around an indexed geometry column inside a WHERE/join predicate prevents the GiST spatial index from being used and forces a full scan. For repeated work, either store the layer in the SRID you query in, or add an indexed generated column:
ALTER TABLE sites
ADD COLUMN geom_3857 geometry(Point, 3857)
GENERATED ALWAYS AS (ST_Transform(geom, 3857)) STORED;
CREATE INDEX ON sites USING GIST (geom_3857);
ST_SetSRID is metadata-only and has no such cost, but it also gives no spatial benefit — it is correctness, not performance.
Common pitfalls and why they happen
- Using
ST_SetSRIDto "convert" CRS. It is the headline bug: the function name sounds like it changes the system, but it only relabels. Coordinates end up plotting in the wrong place with no error raised. - Using
ST_Transformon SRID-0 geometry. Errors out because there is no source definition. Stamp the real SRID withST_SetSRIDfirst. - Transforming geographic data and measuring in degrees anyway. Forgetting to project before
ST_Distance/ST_Areayields garbage units. Transform to a metric CRS first (or use thegeographytype for great-circle distances). - Transform inside hot queries. Defeats the spatial index. Materialise or add a stored generated column.
- Wrong source SRID assumed. If you guess the source CRS wrong before transforming, the output is precisely wrong. Confirm the source with an extent/unit sanity check first.
QA and validation checklist
- Confirm whether you need a relabel (
ST_SetSRID) or a reprojection (ST_Transform) by checking if existing numbers already mean the right thing. - After any CRS operation, check
ST_Extentfalls in the expected unit range (degrees vs metres). - Source SRID is correct and non-zero before any
ST_Transform. - Distance/area computed in a projected CRS or via
geography. - Indexed columns not wrapped in
ST_Transforminside predicates.
Bathyl perspective
We keep a simple rule on every pipeline: ST_SetSRID fixes a label, ST_Transform fixes a position, and they are never substitutes. A one-line extent check after each CRS step catches the silent mislabel before it propagates into joins, buffers and published tiles — the failures that otherwise surface only when a client notices their site is in the ocean.
Related reading
- SRID in PostGIS Explained
- Spatial Indexes in PostGIS
- Why Your GIS Layers Do Not Line Up
- Spatial data products