--- path: docs/spatial-sql-book/04-srid-degrees-metres title: Degrees Are Not Metres desc: SRID, ST_SetSRID, ST_Transform — and the trap that never errors image: /images/spatial-sql-book/04-srid-degrees-metres/cover.webp --- # Degrees Are Not Metres *SRID, ST_SetSRID, ST_Transform — and the trap that never errors* ## What this lesson covers - What an SRID actually is - The query everyone writes first - Three honest fixes: geography, UTM, Web Mercator - Why two of them agree and one inflates - The PROJ setting that fails without an error Coordinates in SRID 4326 are **angles**, not lengths. Every function that returns a distance or an area will still answer — it just answers in degrees, and no error says so. ## What an SRID actually is An SRID is not a setting. It is a **row in a table** — `spatial_ref_sys`, which ships with PostGIS and holds a few thousand coordinate systems, each with a full description of how its numbers map onto the Earth. Three of them matter here: | SRID | What it is | Coordinates in | |---|---|---| | `4326` | WGS 84, plain longitude/latitude — no projection at all | **degrees** | | `3857` | Web Mercator, what slippy map tiles use | metres | | `32654` | UTM zone 54N, the zone Tokyo sits in | metres | Only the first has no `PROJCS` entry, because nothing has been projected: 4326 *is* the globe, described by angles. The other two flatten it, and flattening is where the arithmetic starts working. That single fact drives the whole lesson. `ST_Area` does not know or care what your numbers mean — it multiplies them. Give it degrees and it returns square degrees, a unit that has no fixed size on the ground and shrinks as you move away from the equator. #### An SRID is a row in a table ```sql SELECT srid, auth_name || ':' || auth_srid AS authority, substring(srtext from 'PROJCS\["([^"]+)"') AS projected_as, substring(srtext from 'GEOGCS\["([^"]+)"') AS built_on, (regexp_match(srtext, '.*UNIT\["([^"]+)"'))[1] AS coordinates_are_in FROM spatial_ref_sys WHERE srid IN (4326, 3857, 32654) ORDER BY srid; ``` PostGIS ships `spatial_ref_sys`, and every SRID you have ever pasted into a query is a row in it. Pulling three apart shows what the number stands for: 4326 has no `PROJCS` at all, because nothing is projected — it is the globe in degrees. The other two are projections built on the same datum, and both measure in metres. *[Run this query yourself](https://app.gisowl.com/app/register?source=site)* #### The query everyone writes first ```sql SELECT name, ST_Area(geom) AS area FROM osm.building_polygon WHERE name IS NOT NULL AND name <> '' AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326) ORDER BY area DESC LIMIT 20; ``` Twenty named buildings around Tokyo Station, ordered by area — the query anyone writes on their first day. `ST_Area` accepts the geometry, returns a number, sorts correctly and errors on nothing. The values are tiny, which reads as "very precise" rather than "wrong unit", and the ordering is genuinely correct. Everything about the result encourages you to believe it. *[Run this query yourself](https://app.gisowl.com/app/register?source=site)* | name | sq_degrees | m2_geography | m2_utm54n | m2_mercator | |---|---|---|---|---| | 東京駅一番街 | 0.0000017505 | 17582.5 | 17573.8 | 26706.1 | | 宮殿 | 0.0000016948 | 17021.9 | 17013.6 | 25855.5 | | 日本銀行 | 0.0000014574 | 14637.0 | 14629.8 | 22235.2 | *Real rows from osm.building_polygon (Tokyo Station bbox), PostGIS 3.6.1 / PROJ 9.8.1, 2026-08-22. These are exactly what the lesson's query returns in the editor.* ### Why a degree is not a distance A degree of **latitude** is very nearly constant — about 111 km anywhere on Earth. A degree of **longitude** is not: it is 111 km at the equator, 78 km in Tokyo, and zero at the poles. That is the whole problem. `ST_Area` on a 4326 geometry multiplies one by the other, so its result is neither an area nor a constant fraction of one — it shrinks as you move away from the equator. Two identical buildings, one in Jakarta and one in Reykjavík, return different "areas". ![A degree of latitude is constant; a degree of longitude shrinks towards the poles](/images/spatial-sql-book/04-srid-degrees-metres/degrees-vs-metres.png) *One degree east is not one degree north — and neither is a metre.* ## Three honest fixes, and one that lies Once you accept that 4326 coordinates are angles, there are three ways to get a real measurement — and they do not agree with each other by accident. ```sql ST_Area(geom::geography) -- 1. treat it as a sphere, answer in m² ST_Area(ST_Transform(geom, 32654)) -- 2. project to the local UTM zone, answer in m² ST_Area(ST_Transform(geom, 3857)) -- 3. project to Web Mercator, answer in "m²" ``` **`::geography`** does spherical arithmetic on the ellipsoid. No projection, no zone to pick, correct anywhere on Earth, and slower than the alternatives. It is the right default. **UTM** is a projection designed to be accurate over a narrow strip — sixty zones, six degrees of longitude each. Inside its own zone it is excellent; drag a query across a zone boundary and it degrades. Tokyo is zone 54N. **Web Mercator** is designed for drawing map tiles, not measuring. It preserves angles and inflates area with latitude. That inflation is not a rounding error: at Tokyo's latitude it is a factor of about **1.52**, and it grows the further north you go. So two of the three agree to within a rounding error and the third is half again too big. If your area is coming back suspiciously large and your data went through 3857 at some point, you have found it. #### Measure properly — three ways, side by side ```sql SELECT name, ST_Area(geom) AS sq_degrees, ST_Area(geom::geography) AS m2_geography, ST_Area(ST_Transform(geom, 32654)) AS m2_utm54n, ST_Area(ST_Transform(geom, 3857)) AS m2_mercator FROM osm.building_polygon WHERE name IS NOT NULL AND name <> '' AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326) ORDER BY m2_geography DESC LIMIT 20; ``` The same twenty buildings measured four ways at once: raw degrees, then the three honest answers. Reading across a row is the entire lesson — geography and UTM agree to about one part in two thousand, Web Mercator is half again larger, and the first column is not a unit of area at all. *[Run this query yourself](https://app.gisowl.com/app/register?source=site)* #### Why two agree and one inflates ```sql SELECT name, round(ST_Area(geom::geography)) AS m2_geography, round(ST_Area(ST_Transform(geom, 32654))) AS m2_utm54n, round(ST_Area(ST_Transform(geom, 3857))) AS m2_mercator, round((ST_Area(ST_Transform(geom, 32654)) / ST_Area(geom::geography))::numeric, 4) AS utm_ratio, round((ST_Area(ST_Transform(geom, 3857)) / ST_Area(geom::geography))::numeric, 4) AS mercator_ratio FROM osm.building_polygon WHERE name IS NOT NULL AND name <> '' AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326) ORDER BY m2_geography DESC LIMIT 20; ``` The same three measurements, divided by the spherical answer so the disagreement becomes a ratio. UTM lands within a fraction of a percent — it is designed to be accurate in its own zone, and Tokyo is in zone 54N. Web Mercator comes back around 1.52, and that factor is not noise: it is a property of the projection at this latitude, growing as you move north. *[Run this query yourself](https://app.gisowl.com/app/register?source=site)* #### The buildings you have been measuring ```sql SELECT name, round(ST_Area(geom::geography)) AS m2, geom FROM osm.building_polygon WHERE name IS NOT NULL AND name <> '' AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326) ORDER BY m2 DESC LIMIT 40; ``` The same forty buildings with their true areas in square metres, returned as geometry so you can see what the numbers describe. The largest is a station complex; the smallest are kiosks. Areas that looked like 0.0000017 a moment ago are 17,582 m² — the shapes did not change, only the question. *[Run this query yourself](https://app.gisowl.com/app/register?source=site)* ## The setting that fails without an error Reprojection is not one algorithm. For a datum change, PROJ may need a **grid shift file** — a correction surface describing how one datum's coordinates map to another's. Those files are large, versioned and distributed separately from the library. This server does not fetch them: ```text PROJ 9.4.0 NETWORK_ENABLED=OFF URL_ENDPOINT=https://cdn.proj.org DATABASE_PATH=/usr/share/proj/proj.db ``` `NETWORK_ENABLED=OFF` means PROJ will never reach `cdn.proj.org`. When a transform needs a grid it does not have, it does not stop — it falls back to a lower-accuracy path and returns numbers anyway. No error, no warning, just answers that are a metre or two out. For everything in this course that is harmless: 4326, 3857 and UTM all sit on the WGS 84 datum, so no grid is involved. It matters the moment you bring in national datums — NAD27, OSGB36, Tokyo Datum — where the difference between the correct grid and the fallback can be hundreds of metres. **Check the build before you trust a datum shift.** The version string is the whole story, and it is one query away. #### The PROJ build behind ST_Transform ```sql SELECT PostGIS_PROJ_Version() AS proj_build, substring(PostGIS_PROJ_Version() from 'NETWORK_ENABLED=(\w+)') AS network, substring(PostGIS_PROJ_Version() from 'DATABASE_PATH=(\S+)') AS proj_db, PostGIS_Lib_Version() AS postgis; ``` Every `ST_Transform` you write is executed by PROJ, and its build string says what it can and cannot do. `NETWORK_ENABLED=OFF` means it will never download a grid-shift file, so a datum transform that needs one silently takes a less accurate route instead of failing. Harmless for everything here — 4326, 3857 and UTM share the WGS 84 datum — and important the day you bring in a national one. *[Run this query yourself](https://app.gisowl.com/app/register?source=site)* ## Rules of thumb - `ST_SetSRID` relabels. `ST_Transform` reprojects. They are not interchangeable. - Cast to `::geography` to measure 4326 data — the answer is in metres, anywhere on Earth. - Never measure in 3857. At Tokyo's latitude it inflates area by 1.52×. - Check `PostGIS_PROJ_Version()` before trusting a datum shift — network off means silent fallbacks. --- **Run these queries against a live PostGIS.** [Open the lesson in the app](https://app.gisowl.com/app/register?source=site) — the same SQL, an editor, and your own data. ← [Geometry Is a Type, Not Two Numbers](/docs/spatial-sql-book/03-geometry-as-a-type) · [Reading a Schema You Did Not Write](https://app.gisowl.com/app/register?source=site) → · [All lessons](/docs/spatial-sql-book)