PostGIS Geometry vs Geography: When to Use Each Type
PostGIS Geometry vs Geography: When to Use Each Type
The Problem
PostGIS offers two spatial column types -- geometry and geography -- and choosing the wrong one produces silently incorrect results that are difficult to catch in testing.
The geometry type uses Cartesian math on a flat coordinate plane. The geography type uses geodesic math on a sphere. When you store latitude/longitude coordinates (SRID 4326) in a geometry column, PostGIS treats those coordinates as flat X/Y values. Every spatial function operates in the units of the coordinate system, which for SRID 4326 means degrees -- not meters, not kilometers, degrees.
This is where the damage happens. ST_Distance on a geometry(Point, 4326) column returns the distance in degrees. One degree of latitude is approximately 111 kilometers. A query intended to find "restaurants within 1 kilometer" that uses ST_DWithin(geom, reference_point, 1) actually finds restaurants within 1 degree -- roughly 111 km. The query returns results, no errors are raised, and the application appears to work. It just returns restaurants in the next city over.
The inverse problem is equally dangerous. Using geography when your data is already in a projected coordinate system (UTM, State Plane, British National Grid) forces unnecessary geodesic calculations. The geography type only supports a subset of PostGIS functions, so queries that work fine with geometry fail outright with geography. Developers then resort to casting back and forth between types, adding complexity and losing the benefits of both.
The core rule is simple: if your data covers a large geographic area and you need results in real-world units (meters, square meters), use geography. If your data is in a local projected coordinate system or you need the full PostGIS function set, use geometry. The problem is that most applications start with latitude/longitude in a geometry column because every tutorial begins that way -- and by the time the distance calculations are wrong, the schema is in production.
How to Detect It
Start by auditing what column types exist in your database:
-- List all geometry columns with their SRID and type
SELECT f_table_name, f_geometry_column, type, srid
FROM geometry_columns
ORDER BY f_table_name;
-- List all geography columns
SELECT f_table_name, f_geography_column, type, srid
FROM geography_columns
ORDER BY f_table_name;
The specific pattern to look for is geometry columns storing lat/long coordinates with SRID 4326 -- particularly point data used in proximity queries:
-- Find geometry columns that are likely candidates for geography conversion
SELECT f_table_name, f_geometry_column, type, srid
FROM geometry_columns
WHERE srid = 4326
AND type IN ('POINT', 'MULTIPOINT');
Any table in that result set that participates in ST_Distance, ST_DWithin, or ST_Buffer queries is returning results in degrees, not meters. Check your application queries against these tables. If you see ST_DWithin(geom, point, 0.01) where 0.01 is clearly a developer's attempt to approximate "about 1 kilometer in degrees," you have the problem.
How myDBA.dev Shows It
myDBA.dev's PostGIS monitoring page inventories every spatial column across all databases in your fleet, showing the column type, SRID, and geometry type at a glance. You can immediately see which tables use geometry versus geography and whether SRID 4326 columns exist that might benefit from conversion.

The Queries page surfaces the performance impact. Distance queries on geometry(4326) columns that cast to geography at query time -- ST_Distance(geom::geography, ref::geography) -- show up as significantly slower than native geography operations because the cast prevents index usage. Comparing execution times between geometry-cast and native geography queries makes the cost of the wrong column type visible.

How to Fix It
The safest migration path is a dual-column approach. Add a geography column alongside the existing geometry column, backfill it, create an index, then update application queries to use the new column:
-- Step 1: Add geography column
ALTER TABLE locations ADD COLUMN geog geography(Point, 4326);
-- Step 2: Backfill from existing geometry column
UPDATE locations SET geog = geom::geography;
-- Step 3: Create spatial index on the new column
CREATE INDEX CONCURRENTLY idx_locations_geog
ON locations USING gist (geog);
-- Step 4: Update queries to use geography (distances in meters)
SELECT *
FROM locations
WHERE ST_DWithin(geog, ST_MakePoint(-73.98, 40.75)::geography, 1000);
That ST_DWithin query now correctly finds locations within 1,000 meters. No degree-to-meter approximations, no magic constants.
For queries that need distance values, the result is now in meters:
SELECT name,
ST_Distance(geog, ST_MakePoint(-73.98, 40.75)::geography) AS distance_meters
FROM locations
ORDER BY distance_meters
LIMIT 10;
If you cannot migrate the column type -- perhaps other applications depend on the geometry column -- you can cast at query time:
SELECT ST_Distance(geom::geography, ref_point::geography) AS distance_meters
FROM locations;
This works correctly but cannot use a GiST index on the geometry column. For infrequent queries this is acceptable. For high-traffic proximity searches, the performance penalty makes the full migration worthwhile.
How to Prevent It
Choose the column type before writing the first CREATE TABLE by answering two questions:
Does the data span a large geographic area (multiple countries, global)? Use geography. It handles the curvature of the Earth automatically, returns distances in meters, and works correctly at any latitude. The function set is smaller but covers the common operations: ST_Distance, ST_DWithin, ST_Area, ST_Length, ST_Perimeter, ST_Covers, and ST_Intersects.
Is the data confined to a local region with a projected CRS? Use geometry with the appropriate projected SRID (UTM zone, State Plane, etc.). You get the full PostGIS function set -- ST_Buffer, ST_Union, ST_Intersection, ST_Difference, ST_Simplify, and hundreds more -- with all operations in the CRS's native unit (typically meters). Projected coordinates are already on a flat plane, so Cartesian math is correct.
Storing lat/long and need proximity queries? Use geography. This is the default recommendation for most web applications. Store coordinates as geography(Point, 4326) and all distance operations work in meters out of the box. Only fall back to geometry(Point, 4326) if you need a function that geography does not support and you understand the unit implications.
Document the column type decision in your schema migration comments. When the next developer adds a spatial query, they should not have to guess whether ST_DWithin(col, point, 1000) means 1,000 meters or 1,000 degrees.