PostGIS Basics
PostGIS adds geometry and geography types, spatial functions, and indexes to PostgreSQL. Keep transactional geospatial data in Postgres; push map rendering and heavy tile math to tile servers or client libraries.
Recipe
CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA extensions VERSION '3.5.2';
CREATE TABLE stores (
id serial PRIMARY KEY,
name text NOT NULL,
geom extensions.geometry(Point, 4326) NOT NULL
);
INSERT INTO stores (name, geom) VALUES
('NYC Flagship', extensions.ST_SetSRID(extensions.ST_MakePoint(-73.9857, 40.7484), 4326)),
('LA Branch', extensions.ST_SetSRID(extensions.ST_MakePoint(-118.2437, 34.0522), 4326));
-- Nearby stores within 5 km of a point (geography for meter distances)
SELECT name,
extensions.ST_Distance(
geom::extensions.geography,
extensions.ST_SetSRID(extensions.ST_MakePoint(-73.99, 40.75), 4326)::extensions.geography
) AS meters
FROM stores
ORDER BY meters
LIMIT 10;When to reach for this: Delivery zones, asset tracking, geofencing, and spatial joins where data already lives in your OLTP database.
Working Example
CREATE INDEX stores_geom_gist ON stores USING gist (geom);
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, name
FROM stores
WHERE extensions.ST_DWithin(
geom::extensions.geography,
extensions.ST_SetSRID(extensions.ST_MakePoint(-73.99, 40.75), 4326)::extensions.geography,
5000
);
SELECT extensions.PostGIS_Version();
SELECT extensions.ST_AsGeoJSON(geom) FROM stores WHERE id = 1;What this demonstrates:
geometry(Point, 4326)stores WGS84 lat/lon- Cast to
geographyfor geodesic meter distances - GiST index supports
ST_DWithinbounding-box prefilter ST_AsGeoJSONfeeds web maps without a separate export pipeline
Deep Dive
Postgres vs Tile Server
| Responsibility | Postgres + PostGIS | Tile server (Martin, pg_tileserv, Mapbox) |
|---|---|---|
| Store features | Yes | Reads from Postgres or cache |
| Spatial joins | Yes | No |
| Render map tiles | Possible but not ideal | Optimized MVT/WebP pipeline |
| Transactional updates | Yes | Eventually consistent cache |
| Heavy overlay analytics | SQL | Limited |
Pattern: PostGIS is the system of record. Tile server generates vector tiles for basemaps and overlays. App queries Postgres for search, routing inputs, and business logic.
Core Types
geometry- Planar math in a projected coordinate system; fast for local areas with correct SRID.geography- Spheroid calculations; distances in meters on WGS84.- SRID 4326 - GPS lat/lon default for web maps.
Install Ordering
CREATE EXTENSION postgis;
-- optional add-ons after core
CREATE EXTENSION postgis_topology;
CREATE EXTENSION postgis_raster; -- if raster workloads approvedPin versions in allowlist ADR. Upgrade with ALTER EXTENSION postgis UPDATE and postgis_extensions_upgrade() when release notes require.
API Output
-- GeoJSON for APIs
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(extensions.ST_AsGeoJSON(s.*)::json)
) FROM stores s;Gotchas
- Lat/lon reversed -
ST_MakePoint(lon, lat)not (lat, lon). Fix: Code review convention;ST_Y/ST_Xsanity tests. - Missing SRID - SRID 0 geometries break distance semantics. Fix:
NOT NULLconstraint withgeometry(Point, 4326)andST_SetSRIDon insert. - Geometry for global distance - Wrong units on 4326 geometry. Fix: Cast to
geographyfor meters or project to local UTM. - Serving tiles from raw SQL - Heavy
ST_AsMVTon unindexed tables melts CPU. Fix: Dedicated tile layer with simplified geom and GiST. - Unvalidated WKT - Invalid polygons from user uploads. Fix:
ST_IsValidcheck constraint orST_MakeValidin ingest with audit. - PostGIS in public schema - Clutters search_path. Fix:
WITH SCHEMA extensionson greenfield installs.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Application geolib only | Few static points | Spatial joins across tables |
| Elasticsearch geo | Already on ES for search | Strong FK to relational core |
| Dedicated GIS server (GeoServer) | Complex WMS/WFS standards | Simple REST GeoJSON API |
| BigQuery GIS | Warehouse analytics | OLTP geofencing latency |
FAQs
PostGIS on managed Postgres?
Available on RDS, Cloud SQL, AlloyDB with version matrix. Pin in allowlist.2D vs 3D?
Most apps use 2D Point/Polygon. Z dimensions when elevation matters.Raster extension?
postgis_raster for satellite workflows; separate ops approval and storage planning.MVT in SQL?
ST_AsMVT possible; prefer tile server for cache and simplification.Replication?
Geometry columns replicate normally; verify extension version on replicas.pg_upgrade?
Run postgis_extensions_upgrade after major PG upgrade per docs.JSON vs GeoJSON column?
Store geometry type; emit GeoJSON at API boundary.Mobile offline maps?
Ship tiles or GeoJSON extracts; PostGIS remains authoritative server store.CRS confusion?
Document SRID per table; never mix 4326 and local UTM without transform.Next reads?
Geometry vs geography article for type choice.Related
- Geometry vs Geography - types and distance
- Spatial Indexes - GiST patterns
- PostGIS Performance - simplify and prefilter
- Extensions Basics - install and pin
- PostGIS Best Practices - SRID and validation
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PostGIS 3.5+, pgbouncer 1.x, and Patroni 3.x.