Geospatial Data with PostGIS: Geometry, Geography, and Spatial Queries


Geospatial Data with PostGIS: Geometry, Geography, and Spatial Queries

PostGIS transforms PostgreSQL into a full-featured spatial database. It adds support for geographic objects, spatial indexing, and hundreds of spatial functions. This article covers fundamental concepts and practical query patterns.

Setting Up PostGIS




CREATE EXTENSION postgis;


CREATE EXTENSION postgis_topology;




-- Verify installation


SELECT postgis_version();


-- 3.4.0 or similar





Geometry vs Geography

The most important design decision in PostGIS is choosing between the `geometry` and `geography` data types.

**Geometry** treats coordinates as Cartesian points on a flat plane. It is appropriate for:


* Local datasets where Earth curvature is negligible.

* Data in projected coordinate systems (UTM, State Plane).

* Operations that require planar spatial functions.





CREATE TABLE buildings (


id BIGSERIAL PRIMARY KEY,


name TEXT,


location GEOMETRY(Point, 4326), -- SRID 4326 = WGS84 lon/lat


footprint GEOMETRY(Polygon, 4326)


);




INSERT INTO buildings (name, location) VALUES


('City Hall', ST_SetSRID(ST_MakePoint(-73.9857, 40.7484), 4326));





**Geography** treats coordinates on a sphere (or spheroid). It accounts for Earth curvature, making it appropriate for:


* Global datasets spanning large distances.

* Accurate distance calculations in degrees.

* Queries like "find all points within 10 km."





CREATE TABLE pois (


id BIGSERIAL PRIMARY KEY,


name TEXT,


location GEOGRAPHY(Point, 4326)


);




INSERT INTO pois (name, location) VALUES


('Statue of Liberty', ST_SetSRID(ST_MakePoint(-74.0445, 40.6892), 4326));





**Performance note**: Geography calculations are 2-3x slower than geometry because they use more complex spherical math. For local datasets, cast geography to geometry when precision is not critical.

Spatial Indexes

PostGIS uses GiST (Generalized Search Tree) indexes for spatial data:




CREATE INDEX idx_buildings_location ON buildings USING GIST (location);


CREATE INDEX idx_pois_location ON pois USING GIST (location);





GiST indexes enable indexed spatial operators: `ST_DWithin`, `ST_Intersects`, `ST_Contains`, `ST_Within`, and bounding box comparisons.

Common Spatial Queries

Distance Queries




-- Find all POIs within 1000 meters of a point


SELECT name, ST_Distance(location, ST_SetSRID(ST_MakePoint(-74.006, 40.7128), 4326)) AS dist_meters


FROM pois


WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(-74.006, 40.7128), 4326), 1000)


ORDER BY dist_meters;





`ST_DWithin` uses the index and is far faster than filtering by `ST_Distance` in a WHERE clause.

Spatial Joins




-- Find all buildings within each neighborhood


SELECT n.name AS neighborhood, COUNT(b.id) AS building_count


FROM neighborhoods n


JOIN buildings b ON ST_Contains(n.geom, b.location)


GROUP BY n.name


ORDER BY building_count DESC;





Area Calculations




-- Calculate building footprint areas in square meters


SELECT name, ST_Area(footprint::geography) AS area_sqm


FROM buildings


ORDER BY area_sqm DESC;





Nearest Neighbor Search

The `<->` operator enables efficient nearest-neighbor searches using the GiST index:




SELECT name, location,


location <-> ST_SetSRID(ST_MakePoint(-73.9857, 40.7484), 4326) AS dist


FROM buildings


ORDER BY location <-> ST_SetSRID(ST_MakePoint(-73.9857, 40.7484), 4326)


LIMIT 5;





Data Import and Export

Importing GeoJSON




-- Cast GeoJSON to geometry


INSERT INTO pois (name, location)


SELECT


properties ->> 'name',


ST_SetSRID(ST_GeomFromGeoJSON(properties ->> 'location'), 4326)


FROM jsonb_array_elements('...geojson array...') AS features;





Importing Shapefiles




shp2pgsql -s 4326 -I -g geom neighborhoods.shp public.neighborhoods | psql -d mydb





Exporting as GeoJSON




SELECT row_to_json(fc) AS geojson


FROM (


SELECT


'FeatureCollection' AS type,


json_agg(f) AS features


FROM (


SELECT


'Feature' AS type,


ST_AsGeoJSON(location)::jsonb AS geometry,


jsonb_build_object('name', name) AS properties


FROM pois


) AS f


) AS fc;





Common Spatial Functions




-- Transform between coordinate systems


SELECT ST_Transform(location, 3857) FROM buildings; -- to Web Mercator


SELECT ST_Transform(location, 32618) FROM buildings; -- to UTM zone 18N




-- Simplify geometry for map display


SELECT ST_Simplify(footprint, 1.0) FROM buildings; -- 1.0 threshold




-- Buffer around a point


SELECT ST_Buffer(location::geography, 500) AS five_hundred_m_buffer FROM pois;




-- Compute convex hull


SELECT ST_ConvexHull(ST_Collect(location)) FROM pois;




-- Intersection of two geometries


SELECT ST_Intersection(n.geom, b.footprint)


FROM neighborhoods n, buildings b


WHERE ST_Intersects(n.geom, b.footprint);





Performance Optimization




-- Cluster a table on its spatial index for faster range scans


CLUSTER buildings USING idx_buildings_location;


ANALYZE buildings;




-- Use ST_Subdivide for very large geometries


CREATE TABLE neighborhoods_subdivided AS


SELECT id, name,


ST_SubDivide(geom, 200) AS geom


FROM neighborhoods;




CREATE INDEX idx_neighborhoods_subdivided ON neighborhoods_subdivided USING GIST (geom);





PostGIS vs Other Tools

| Feature | PostGIS | MongoDB 2dsphere | Elasticsearch Geo | |---------|---------|------------------|-------------------| | CRS support | 5000+ SRIDs | WGS84 only | WGS84 only | | Spatial functions | 400+ | 30+ | 20+ | | Topology | Full support | None | None | | Raster support | Yes (PostGIS raster) | No | No | | Routing | pgRouting extension | No | No |

PostGIS is the most complete open-source spatial database. Use it as the system of record for all geospatial data, and only replicate to specialized tools (map rendering engines, geocoding services) when necessary. Start with geography types for global datasets and geometry for local projections, and always verify index usage in EXPLAIN plans for spatial queries.