PostGIS Distance Calculations: Why ST_Distance Returns Degrees Instead of Meters

PostGIS Distance Calculations: Why ST_Distance Returns Degrees Instead of Meters

The Problem

You build a "find restaurants within 1 kilometer" feature. The query looks right:

SELECT name, ST_Distance(location, ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326)) AS distance
FROM restaurants
WHERE ST_Distance(location, ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326)) < 1000
ORDER BY distance;

It returns zero rows. Or it returns every restaurant on the continent. The query runs without error, but the results are wrong -- and the reason is that ST_Distance on a geometry(Point, 4326) column returns degrees, not meters.

SRID 4326 (WGS 84) stores coordinates as longitude/latitude in degrees. When you call ST_Distance on two geometry values, PostGIS performs Cartesian math -- it computes the straight-line distance in the coordinate system's native units. For SRID 4326, those units are degrees. The distance between Times Square and Central Park comes back as approximately 0.019 -- not 2,100 meters, but 0.019 degrees.

This is the single most common PostGIS mistake, and it is dangerously silent. There is no error, no warning, no type mismatch. The function happily computes a number. That number just does not mean what you think it means.

The confusion is compounded by the fact that the relationship between degrees and meters is not constant. One degree of latitude is approximately 111 km everywhere on Earth. But one degree of longitude varies from 111 km at the equator to 0 km at the poles. At New York City's latitude (~40.7N), one degree of longitude is roughly 85 km. So a distance of 0.01 degrees means approximately 1.1 km north-south but only 0.85 km east-west -- and this ratio changes depending on where on the planet you are.

Any application that uses ST_Distance on geometry(4326) and compares the result to a threshold in meters is broken. It may appear to work in testing (because the numbers are small and nobody checks the units), but it will produce incorrect results in production -- returning too many or too few results depending on the threshold value and the latitude of the data.

How to Detect It

Check your spatial columns to identify which tables use geometry with SRID 4326 versus geography:

-- List all geometry columns with their SRIDs
SELECT f_table_name, f_geometry_column, type, srid, coord_dimension
FROM geometry_columns
ORDER BY f_table_name;

-- List all geography columns (always meter-based)
SELECT f_table_name, f_geography_column, type, srid
FROM geography_columns
ORDER BY f_table_name;

If you see geometry columns with srid = 4326, any distance or area calculation on those columns returns degrees, not meters. Confirm the behavior directly:

-- This returns ~0.019 degrees, NOT meters
SELECT ST_Distance(
    ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326),
    ST_SetSRID(ST_MakePoint(-73.972, 40.764), 4326)
);

Then search your query history for distance calculations that compare against numeric thresholds -- those thresholds are almost certainly intended as meters but are being compared against degrees.

How myDBA.dev Shows It

PostGIS page showing geometry vs geography column types and SRID distribution

myDBA.dev's PostGIS monitoring page displays every spatial column across all tables, with the column type (geometry or geography), SRID, and geometry type clearly visible. When you see a geometry(Point, 4326) column, you know immediately that distance functions on that column return degrees. The SRID distribution chart shows how many columns use each coordinate reference system, making it easy to spot tables that may be affected.

Queries page showing distance calculation queries with execution times

The Queries page complements this by surfacing the actual spatial queries running against your database. You can identify queries using ST_Distance or ST_DWithin on geometry columns, check their execution times, and trace back to the specific tables involved. If a distance query runs faster than expected, it may be because it is doing cheap Cartesian math instead of the geodesic calculation you actually need.

How to Fix It

There are three approaches, each suited to different situations:

Option 1: Cast to geography on the fly. This is the quickest fix and requires no schema changes. Casting geometry to geography makes PostGIS use geodesic (great-circle) math and return meters:

-- Cast to geography for meter-based distance
SELECT
    name,
    ST_Distance(
        location::geography,
        ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326)::geography
    ) AS distance_meters
FROM restaurants
WHERE ST_DWithin(
    location::geography,
    ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326)::geography,
    1000  -- 1000 meters
)
ORDER BY distance_meters;

The downside is performance. The cast prevents PostgreSQL from using a spatial index on the geometry column. For small tables this is acceptable; for millions of rows, it is not.

Option 2: Use the geography type natively. If your data represents locations on Earth and you need meter-based calculations, change the column type to geography:

-- Add a geography column
ALTER TABLE restaurants ADD COLUMN location_geog geography(Point, 4326);

-- Populate from existing geometry column
UPDATE restaurants SET location_geog = location::geography;

-- Create a spatial index on the geography column
CREATE INDEX idx_restaurants_geog ON restaurants USING GIST (location_geog);

-- Now ST_Distance returns meters natively
SELECT name, ST_Distance(location_geog, ST_MakePoint(-73.985, 40.748)::geography) AS distance_meters
FROM restaurants
WHERE ST_DWithin(location_geog, ST_MakePoint(-73.985, 40.748)::geography, 1000)
ORDER BY distance_meters;

Geography columns use geodesic math by default, support spatial indexing, and ST_Distance returns meters without any casting. This is the correct long-term solution for most applications dealing with Earth coordinates.

Option 3: Transform to a projected CRS. For regional datasets where you need high accuracy and fast Cartesian math, transform your coordinates to a local projected coordinate system (like UTM) where the native units are meters:

-- Transform to UTM Zone 18N (SRID 32618) for NYC area
SELECT
    name,
    ST_Distance(
        ST_Transform(location, 32618),
        ST_Transform(ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326), 32618)
    ) AS distance_meters
FROM restaurants;

This approach gives the fastest computation (Cartesian math in meters) but only works accurately within the chosen projection's coverage area. UTM Zone 18N is accurate for the northeastern US; data in California or Europe needs a different zone.

How to Prevent It

Make geography the default column type for any table storing locations on Earth. Reserve geometry for data that is genuinely planar -- CAD drawings, game maps, floor plans -- or for cases where you have explicitly chosen a projected CRS with meter-based units.

Establish a project convention: if a column stores latitude/longitude coordinates, it must be geography(Point, 4326), not geometry(Point, 4326). Enforce this in code review and migration review. The two types look almost identical in a schema definition, but they produce fundamentally different behavior from every spatial function.

Add a check to your test suite for distance queries. Compute a known distance (e.g., two points 1 km apart) and assert that the result is within a reasonable range of 1000 meters, not 0.009 degrees. This catches the geometry/geography confusion immediately.

myDBA.dev's PostGIS monitoring surfaces column types and SRIDs across all your tables, making it straightforward to audit your schema for geometry columns that should be geography. Catching this at the schema level -- before queries are written against the wrong type -- prevents the entire class of unit confusion bugs.