Short answer

An SRID (Spatial Reference Identifier) is an integer stored alongside every geometry in PostGIS that says which coordinate reference system the coordinates are expressed in. It is a label and a lookup key, not a transformation. PostGIS uses it for two things: to refuse operations between geometries in different systems (preventing you from intersecting metres with degrees), and to look up the CRS definition in the spatial_ref_sys table when a transformation is requested. Get the SRID wrong and your data is in the wrong place or your queries silently return nothing.

SRID, EPSG, and spatial_ref_sys

In everyday PostGIS the SRID is the EPSG code. EPSG is a catalogue maintained by the IOGP of coordinate reference systems, each with a unique number — 4326 for WGS84 geographic, 3857 for Web Mercator, 32631 for WGS84 / UTM zone 31N. PostGIS ships these in a system table:

SELECT srid, auth_name, srtext, proj4text
FROM spatial_ref_sys
WHERE srid = 4326;

spatial_ref_sys holds, per SRID, the authority name (usually EPSG), the WKT definition (srtext) and a PROJ string (proj4text). When you call ST_Transform(geom, 32631), PostGIS reads the source SRID from the geometry, reads target SRID 32631 from this table, and asks the PROJ library to compute the coordinate operation. If an SRID is not present in spatial_ref_sys, transformations to or from it fail. You can add custom CRS definitions by inserting a row, but for standard work the EPSG entries are already there.

How the SRID is stored

The SRID lives in two places, and they should agree:

  1. On the geometry value itself — every PostGIS geometry carries its SRID, returned by ST_SRID(geom).
  2. In the column's type modifier — a typed column declared geometry(Point, 4326) constrains all values to SRID 4326 and rejects mismatches on insert.

Declaring the typed column is best practice. It turns a whole class of "wrong CRS" bugs into immediate insert errors instead of silent corruption discovered weeks later. Check both with:

SELECT ST_SRID(geom) AS value_srid FROM roads LIMIT 1;

SELECT f_geometry_column, srid, type
FROM geometry_columns
WHERE f_table_name = 'roads';

SRID 0 and the "unknown CRS" problem

SRID 0 means undefined / unknown. It commonly appears when geometry is built from raw coordinates without specifying a system, or imported from a source that had no .prj. Geometry at SRID 0 will still draw, but it cannot be transformed (PostGIS has no idea what the numbers mean) and it will mismatch any properly-labelled layer.

The fix depends on why it is 0:

  • You know the coordinates are already in some CRS, just unlabelled. Stamp the correct SRID with ST_SetSRID — this changes the label only, moving no coordinates. Example: data is clearly lon/lat values but tagged 0 → UPDATE pts SET geom = ST_SetSRID(geom, 4326);
  • The coordinates are in the wrong system and need to move. That is a transformation job, and you must first label them correctly, then transform. ST_Transform on an SRID-0 geometry errors out because there is no source definition.

Never use ST_Transform to "fix" an SRID-0 geometry directly, and never use ST_SetSRID to convert metres to degrees — those are the two halves of the most common SRID bug, covered in depth in the companion article on ST_Transform vs ST_SetSRID.

The SRID mismatch error

PostGIS spatial predicates require matching SRIDs. This:

SELECT * FROM parcels p, floodzone f
WHERE ST_Intersects(p.geom, f.geom);

raises ERROR: Operation on mixed SRID geometries if parcels is 4326 and floodzone is 32631. The error is a feature: it stops you computing a geometric relationship between degrees and metres, which would be meaningless. The fix is to transform one side into the other's system — and for distance/area work, transform both into a projected CRS so results are in metres:

SELECT *
FROM parcels p
JOIN floodzone f
  ON ST_Intersects(
       ST_Transform(p.geom, 32631),
       ST_Transform(f.geom, 32631)
     );

Note that wrapping ST_Transform around an indexed column can defeat the spatial index. The performant pattern is to materialise both layers in the same SRID once (or add a transformed generated column with its own GiST index) rather than transforming inside every query.

Worked example: loading data and getting the SRID right

Loading a shapefile in EPSG:27700 (British National Grid) into PostGIS with ogr2ogr:

ogr2ogr -f PostgreSQL "PG:dbname=gis" sites_bng.shp \
  -nln sites -a_srs EPSG:27700 -lco GEOMETRY_NAME=geom \
  -nlt PROMOTE_TO_MULTI

-a_srs assigns the SRID (use it when the source .prj is missing or wrong); use -t_srs instead if you want to reproject during load. Verify after:

SELECT DISTINCT ST_SRID(geom) FROM sites;   -- expect 27700
SELECT ST_Extent(geom) FROM sites;          -- expect easting/northing in metres

If ST_Extent returns values near 0–1 (degrees) when you expected hundreds of thousands (metres), the SRID and the actual coordinates disagree — stop and diagnose before building anything on top.

Common pitfalls and why they happen

  • Untyped geometry columns. geometry with no (type, srid) modifier lets any SRID in, so a single mislabelled insert poisons the table. Declare geometry(MultiPolygon, 27700).
  • SRID 0 left in place. Data draws, so nobody notices until a transform or join fails. Audit with SELECT DISTINCT ST_SRID(geom).
  • Transforming inside the query on indexed columns. Kills index use and slows joins. Store layers in a common SRID or index a transformed column.
  • Assuming SRID equals correctness. A geometry can be labelled 4326 while holding UTM metres — the label is right, the coordinates are wrong. Check the extent matches the expected unit range.
  • Custom CRS not in spatial_ref_sys. Transformations to an SRID absent from the table fail; insert the definition first.

QA and validation checklist

  • Geometry columns declared with explicit (type, srid).
  • SELECT DISTINCT ST_SRID(geom) returns the single expected code, never 0.
  • Extent values fall in the expected unit range for that CRS (degrees vs metres).
  • Joins/predicates run in a shared SRID; distance/area work done in a projected CRS.
  • Any custom SRID present in spatial_ref_sys before use.

Bathyl perspective

We treat the SRID as a contract: typed geometry columns, one declared CRS per table, and a load-time extent check that catches mislabelled coordinates before they reach a query. A database where every geometry's SRID is both present and true is the foundation everything else — joins, tiles, analysis — quietly depends on.

Related reading

Sources