PostGIS: Spatial Data in Postgres
PostGIS is the spatial extension for Postgres. It adds geographic data types, spatial indexing, and hundreds of functions for querying location data — making Postgres the gold standard for spatial databases.
With PostGIS you can:
- Store points, lines, polygons, and complex geometries
- Calculate distances, areas, intersections, and containment
- Find nearest neighbors efficiently with spatial indexes
- Transform between coordinate systems
- Process raster (grid) data alongside vector geometries
PostGIS powers mapping applications, logistics platforms, delivery routing, land-use analysis, and anything that involves the question "what's near what?"
CREATE EXTENSION postgis; -- Confirm the version SELECT PostGIS_Full_Version();
Geometry vs. Geography
PostGIS offers two spatial types, and choosing the right one matters:
Geometry
Operates on a flat Cartesian plane. Coordinates are treated as simple X/Y numbers with no awareness of the Earth's curvature.
- Fast calculations (simple math)
- Results are in the coordinate system's units (often degrees for EPSG:4326, meters for projected systems)
- Best for: small areas where curvature doesn't matter, projected coordinate systems, or when you need the full range of PostGIS functions
-- Geometry column — coordinates are just numbers CREATE TABLE parcels ( id SERIAL PRIMARY KEY, name TEXT, boundary GEOMETRY(Polygon, 4326) );
Geography
Operates on a sphere (or spheroid). It understands that the Earth is round and calculates true great-circle distances.
- Results are always in meters
- More accurate over large distances
- Fewer functions available (but the most common ones work)
- Best for: points spread across the globe, when you need real-world distances in meters
-- Geography column — calculations account for Earth's curvature CREATE TABLE airports ( id SERIAL PRIMARY KEY, code CHAR(3), name TEXT, location GEOGRAPHY(Point, 4326) );
When to Use Which
| Scenario | Use |
|---|---|
| Points spread across the globe | Geography |
| Need distances in meters | Geography |
| Small area (single city/state) | Either works |
| Need advanced functions (ST_Union, ST_Difference) | Geometry |
| Working with a projected coordinate system (UTM, State Plane) | Geometry |
| Storing routes/polygons for large regions | Geometry with ST_Transform |
aside positive Rule of thumb: if your data is lat/lon points and you mainly need distance and containment queries, use Geography. If you need the full spatial toolkit or a projected coordinate system, use Geometry.
Creating Spatial Tables
Point Data
The most common case — storing locations:
CREATE TABLE coffee_shops ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, address TEXT, location GEOGRAPHY(Point, 4326) ); -- Insert using longitude, latitude (note: lon first, lat second!) INSERT INTO coffee_shops (name, address, location) VALUES ('Blue Bottle', '315 Linden St, SF', ST_Point(-122.4194, 37.7749)), ('Stumptown', '128 SW 3rd Ave, Portland', ST_Point(-122.6587, 45.5155)), ('Intelligentsia', '53 E Randolph St, Chicago', ST_Point(-87.6298, 41.8781));
aside negative A common gotcha: PostGIS uses longitude, latitude order (X, Y) — not the lat, lon order you see on Google Maps. Getting this backwards puts your points in the ocean.
Line Data
For roads, routes, or paths:
CREATE TABLE trails ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, difficulty TEXT, path GEOMETRY(LineString, 4326) ); INSERT INTO trails (name, difficulty, path) VALUES ('Forest Loop', 'moderate', ST_GeomFromText('LINESTRING(-122.68 45.52, -122.69 45.53, -122.68 45.54)', 4326));
Polygon Data
For boundaries, zones, or areas:
CREATE TABLE neighborhoods ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, boundary GEOMETRY(Polygon, 4326) ); INSERT INTO neighborhoods (name, boundary) VALUES ('Pearl District', ST_GeomFromText('POLYGON((-122.685 45.525, -122.675 45.525, -122.675 45.530, -122.685 45.530, -122.685 45.525))', 4326));
SRID and Coordinate Reference Systems
Every spatial column has an SRID (Spatial Reference System Identifier) that tells PostGIS how to interpret the coordinates.
The most common SRIDs:
| SRID | Name | Units | Use Case |
|---|---|---|---|
| 4326 | WGS 84 | Degrees | GPS data, web mapping, global datasets |
| 3857 | Web Mercator | Meters | Web map tiles (Google Maps, Mapbox) |
| 32610 | UTM Zone 10N | Meters | Pacific Northwest US |
| 2163 | US National Atlas | Meters | Continental US equal-area |
-- Check what SRIDs are available SELECT srid, proj4text FROM spatial_ref_sys WHERE srid IN (4326, 3857, 32610); -- Transform between coordinate systems SELECT ST_Transform( ST_SetSRID(ST_Point(-122.4194, 37.7749), 4326), 3857 ) AS mercator_point;
Always specify the SRID when creating columns — it ensures PostGIS validates your data and transforms correctly:
-- This enforces SRID 4326 on all data in the column ALTER TABLE coffee_shops ADD CONSTRAINT enforce_srid CHECK (ST_SRID(location::geometry) = 4326);
Core Spatial Functions
Distance
-- Distance between two points (geography = meters) SELECT ST_Distance( ST_Point(-122.4194, 37.7749)::geography, ST_Point(-122.6587, 45.5155)::geography ) AS distance_meters; -- Returns ~863,476 meters (SF to Portland) -- Find all shops within 2km of a location SELECT name, round(ST_Distance( location, ST_Point(-122.4194, 37.7749)::geography )::numeric) AS distance_m FROM coffee_shops WHERE ST_DWithin( location, ST_Point(-122.4194, 37.7749)::geography, 2000 -- 2km radius ) ORDER BY distance_m;
Containment and Intersection
-- Is a point inside a polygon? SELECT ST_Contains( boundary, ST_SetSRID(ST_Point(-122.681, 45.527), 4326) ) AS is_inside FROM neighborhoods WHERE name = 'Pearl District'; -- Find all coffee shops in a neighborhood SELECT c.name AS shop, n.name AS neighborhood FROM coffee_shops c JOIN neighborhoods n ON ST_Contains(n.boundary, c.location::geometry); -- Do two geometries overlap? SELECT ST_Intersects( a.boundary, b.boundary ) AS overlaps FROM neighborhoods a, neighborhoods b WHERE a.name = 'Pearl District' AND b.name = 'Old Town';
Area and Length
-- Area of a polygon (geography = square meters) SELECT name, round(ST_Area(boundary::geography)::numeric) AS area_sqm FROM neighborhoods; -- Length of a line (geography = meters) SELECT name, round(ST_Length(path::geography)::numeric) AS length_m FROM trails;
Buffer and Envelope
-- Create a 1km buffer zone around a point SELECT ST_Buffer( ST_Point(-122.4194, 37.7749)::geography, 1000 -- 1km ) AS buffer_zone; -- Bounding box of a geometry SELECT ST_Envelope(boundary) AS bbox FROM neighborhoods WHERE name = 'Pearl District';
Nearest Neighbor
The K-nearest-neighbor (KNN) operator uses the spatial index for fast lookups:
-- Find 5 nearest coffee shops to a location (uses index!) SELECT name, round(ST_Distance( location, ST_Point(-122.68, 45.52)::geography )::numeric) AS distance_m FROM coffee_shops ORDER BY location <-> ST_Point(-122.68, 45.52)::geography LIMIT 5;
aside positive The
<->operator performs an index-assisted nearest-neighbor search. It's dramatically faster than computing all distances and sorting. Always use it withORDER BY ... LIMITfor KNN queries.
Spatial Indexing
Without an index, every spatial query does a sequential scan — comparing your query geometry against every row. With millions of features, this is unbearable.
GiST Indexes
GiST (Generalized Search Tree) is the standard spatial index. It uses bounding boxes to quickly eliminate rows that can't possibly match.
-- Create a spatial index on the geography column CREATE INDEX idx_shops_location ON coffee_shops USING GIST (location); -- For geometry columns CREATE INDEX idx_neighborhoods_boundary ON neighborhoods USING GIST (boundary);
BRIN Indexes (for sorted data)
If your spatial data has natural ordering (e.g., imported in geographic clusters), a BRIN index uses far less space:
-- BRIN index — compact but requires physically sorted data CREATE INDEX idx_points_location_brin ON large_point_table USING BRIN (geom);
Index Performance Tips
- Always
ANALYZEafter bulk inserts so the planner knows about the index - Use
ST_DWithininstead ofST_Distance < X— the former can use the index, the latter often cannot - For KNN queries, use the
<->operator which the GiST index accelerates - Check with
EXPLAIN ANALYZEthat your queries actually use the index
EXPLAIN ANALYZE SELECT name FROM coffee_shops WHERE ST_DWithin(location, ST_Point(-122.68, 45.52)::geography, 5000);
Common Patterns
Geofencing
Determine which zone a point falls in — useful for pricing zones, delivery areas, or jurisdictions:
-- Which delivery zone is this address in? SELECT z.name AS zone, z.delivery_fee FROM delivery_zones z WHERE ST_Contains(z.boundary, ST_SetSRID(ST_Point(-122.43, 37.77), 4326));
Clustering Points
Group nearby points together:
-- Cluster coffee shops within 500m of each other SELECT ST_ClusterDBSCAN(location::geometry, eps := 0.005, minpoints := 2) OVER () AS cluster_id, name FROM coffee_shops;
Route Length and Bounding
-- Total length of a multi-segment route SELECT ST_Length( ST_MakeLine(ARRAY[ ST_Point(-122.68, 45.52), ST_Point(-122.69, 45.53), ST_Point(-122.67, 45.54) ])::geography ) AS route_length_m;
Simplifying Complex Geometries
When rendering on a map, you don't need millimeter precision:
-- Simplify a complex boundary for display SELECT ST_Simplify(boundary, 0.001) AS simplified FROM neighborhoods;
Routing with pgRouting
pgRouting extends PostGIS with geospatial routing and network analysis. It adds graph algorithms on top of your spatial data — turning a road network stored in PostGIS into a routable graph without leaving the database.
Core Algorithms
| Algorithm | Function | Use Case |
|---|---|---|
| Dijkstra | pgr_dijkstra() | Shortest path between two nodes |
| A* | pgr_aStar() | Heuristic shortest path (faster for large graphs) |
| K-Shortest Paths | pgr_ksp() | Multiple alternative routes |
| Driving Distance | pgr_drivingDistance() | Service areas / isochrones |
| TSP | pgr_TSP() | Optimal visit order for multiple stops |
Basic Shortest Path
-- Enable pgRouting CREATE EXTENSION pgrouting; -- Find shortest path between nodes 1 and 42 SELECT seq, node, edge, cost, agg_cost FROM pgr_dijkstra( 'SELECT id, source, target, cost, reverse_cost FROM roads', 1, -- start node 42, -- end node directed := true );
Building a Routable Network from OpenStreetMap
The osm2pgrouting tool imports OpenStreetMap data into a pgRouting-compatible schema:
osm2pgrouting \ --file portland.osm \ --dbname routing_db \ --conf mapconfig.xml
Once loaded, your road network has source and target node IDs that pgRouting's algorithms traverse.
Service Areas (Isochrones)
Calculate all points reachable within a cost threshold — useful for "how far can I drive in 10 minutes?" analysis:
-- All nodes reachable within cost 15 from node 1 SELECT node, edge, cost, agg_cost FROM pgr_drivingDistance( 'SELECT id, source, target, cost FROM roads', 1, -- start node 15.0 -- max cost );
When to Use pgRouting vs. PostGIS Alone
PostGIS gives you straight-line distances and spatial relationships. pgRouting adds:
- Path traversal along a network (roads follow curves, not straight lines)
- Cost modeling (traffic, speed limits, road class)
- Turn restrictions
- One-way streets
- Vehicle constraints (weight limits, road access)
If your question is "what's the shortest path along roads?" rather than "what's the straight-line distance?", you need pgRouting.
Serving Spatial Data: pg_tileserv and pg_featureserv
Two lightweight Go microservices from Crunchy Data turn PostGIS into a spatial web backend with zero middleware:
pg_tileserv — Vector Tiles from PostGIS
pg_tileserv is a thin tile server that takes HTTP tile requests, executes SQL against PostGIS, and returns Mapbox Vector Tiles (MVT). Any table with a geometry column is auto-published as a tile layer.
- Serves MVT tiles for interactive map clients (Mapbox GL, MapLibre, Leaflet)
- Table layers — auto-discovers any table with a geometry column
- Function layers — expose custom SQL functions as dynamic tile sources
- Uses the database's existing security model for access control
- Stateless and extremely lightweight
-- Create a function layer for pg_tileserv CREATE OR REPLACE FUNCTION public.points_in_radius( z integer, x integer, y integer, center_lon float8 DEFAULT -122.4, center_lat float8 DEFAULT 37.8, radius float8 DEFAULT 1000 ) RETURNS bytea AS $$ DECLARE result bytea; bounds geometry; BEGIN bounds := ST_TileEnvelope(z, x, y); SELECT ST_AsMVT(q, 'points_in_radius', 4096, 'geom') INTO result FROM ( SELECT ST_AsMVTGeom(geom, bounds) AS geom, name FROM my_points WHERE ST_DWithin(geom::geography, ST_Point(center_lon, center_lat)::geography, radius) AND ST_Intersects(geom, bounds) ) q; RETURN result; END; $$ LANGUAGE plpgsql STABLE;
pg_featureserv — GeoJSON Features via REST
pg_featureserv implements the OGC API - Features standard, exposing PostGIS tables as GeoJSON collections over HTTP:
- RESTful API returning GeoJSON
- CQL filtering for attribute and spatial queries
- Supports both reading and writing features
- Exposes PostGIS spatial functions as API endpoints
- Minimal configuration — point it at a database and it auto-discovers tables
GET /collections/coffee_shops/items?filter=name LIKE 'Blue%' GET /collections/coffee_shops/items?bbox=-122.5,37.7,-122.3,37.9 GET /functions/points_in_radius/items?center_lon=-122.4¢er_lat=37.8&radius=500
Architecture Philosophy
Both services follow a database-centric design: the database is the single source of truth and the coordination layer. The microservices are stateless HTTP frontends — they add no business logic, cache no state, and rely on PostGIS for all spatial processing. This means you can change your data model, add columns, or create new functions, and they're immediately available through the API without redeployment.
Advanced Spatial Topics
H3 Hexagonal Indexing
H3 (Uber's hierarchical hexagonal grid system) pairs well with PostGIS for aggregating point data into uniform cells:
-- Requires h3-pg extension CREATE EXTENSION h3; -- Index points into H3 cells at resolution 9 SELECT h3_lat_lng_to_cell(location::point, 9) AS h3_index, count(*) AS point_count FROM coffee_shops GROUP BY 1;
H3 cells are useful for heatmaps, density analysis, and consistent spatial binning that avoids the distortion of square grids at high latitudes.
Topology
PostGIS Topology stores data as faces, edges, and nodes with shared boundaries — enforcing topological rules (no gaps, no overlaps) at the database level:
CREATE EXTENSION postgis_topology; -- Create a topology with 1-meter tolerance SELECT topology.CreateTopology('city_topo', 4326, 0.00001); -- Add a layer SELECT topology.AddTopoGeometryColumn('city_topo', 'public', 'neighborhoods', 'topo_geom', 'POLYGON');
Topology is essential for cadastral data, administrative boundaries, and any dataset where shared borders must remain consistent.
3D and SFCGAL
PostGIS has growing support for 3D operations via the SFCGAL library:
CREATE EXTENSION postgis_sfcgal; -- 3D intersection SELECT ST_3DIntersection(geom_a, geom_b) FROM buildings; -- Extrude a 2D polygon to 3D SELECT ST_Extrude(boundary, 0, 0, building_height) FROM footprints;
Raster Data
PostGIS can store and analyze gridded (raster) data alongside vector geometries:
-- Query raster values at point locations SELECT ST_Value(r.rast, p.geom) AS elevation FROM elevation_raster r JOIN trail_points p ON ST_Intersects(r.rast, p.geom);
This is powerful for elevation analysis, land cover classification, and combining satellite imagery with vector boundaries.
The PostGIS Community
PostGIS is governed by a Project Steering Committee (PSC) that coordinates the project's direction, release cycles, and community. This is operated under the Open Source Geospatail Foundation (OSGeo) and is managed seperately from PostgreSQL. The project's co-founder and steering council member Paul Ramsey is a Snowflake engineer.
PostGIS Day
PostGIS Day is a free online event held every year on the Thursday of the third week of November (the day after GIS Day) hosted here at Snowflake. It brings together developers, users, and enthusiasts for a full day of 30-minute talks covering everything from beginner tutorials to cutting-edge spatial techniques.
- When: Third Thursday of November, annually (2025 was the 7th annual event)
- Format: Free virtual drop-in via Zoom, talks recorded and posted to YouTube
- Duration: ~11 hours (afternoon in Europe through afternoon Pacific time)
- Website: postgisday.rocks
- Videos: PostGIS Day 2025 (Snowflake YouTube) | PostGIS Day archives (Crunchy Data YouTube)
PostGIS Day 2025 was hosted by Paul Ramsey and Elizabeth Christensen and supported by Snowflake. Topics ranged from AI agents for spatial SQL to lakehouse architectures with GeoParquet, industrial-scale disaster response platforms, and high school students building isochrone analysis for emergency services. Read the full PostGIS Day 2025 recap on the Snowflake blog.
PostGIS on Snowflake Postgres
aside positive PostGIS is available on Snowflake Postgres. Enable it with
CREATE EXTENSION postgis;. See the Snowflake Postgres extensions documentation for version details.
On Snowflake Postgres, PostGIS works like any other Postgres deployment — create the extension, build your spatial tables, and query them. The extension is pre-installed; you just enable it.
-- Enable PostGIS on Snowflake Postgres CREATE EXTENSION IF NOT EXISTS postgis; -- Verify SELECT PostGIS_Version();
Conclusion
PostGIS is the most mature and capable spatial database extension available. It handles everything from simple "find nearby" queries to complex geometric operations across millions of features. The combination of spatial types, a rich function library, and GiST indexing means you rarely need a separate GIS system — Postgres can be your spatial database too.
Related Resources
Complete function reference and usage guides for all PostGIS versions.
Routing algorithms, network analysis, and shortest-path functions for PostGIS.
Lightweight vector tile server — serves Mapbox Vector Tiles directly from PostGIS.
RESTful GeoJSON feature server implementing OGC API - Features for PostGIS.
Annual free online event celebrating PostGIS — talks, demos, and community.
Full playlist of PostGIS Day 2025 talks hosted by Snowflake.
Previous years' PostGIS Day recordings from Crunchy Data.
Interactive browser-based playground for learning PostGIS fundamentals hands-on.
Tutorials on spatial performance, tile serving, raster analysis, and more.
This content is provided as is, and is not maintained on an ongoing basis. It may be out of date with current Snowflake instances