PostGIS is the leading open-source spatial database extension for PostgreSQL. It transforms a standard relational database into a full-featured geographic information system capable of storing, indexing, and querying spatial data with SQL. If you work with geospatial data — whether you are building a web mapping application, analyzing urban infrastructure, or processing satellite-derived datasets — PostGIS spatial queries are a fundamental skill. This tutorial introduces PostGIS from the ground up: creating spatial tables, understanding geometry versus geography types, importing data, running essential spatial queries, building indexes, and exporting results as GeoJSON.
What Is PostGIS?
PostGIS is a spatial extension for PostgreSQL that adds support for geographic objects. It provides geometry and geography data types, spatial indexing via GiST (Generalized Search Tree), and over 300 functions for spatial analysis. PostGIS implements the OGC Simple Features specification and is used by organizations ranging from OpenStreetMap to the United Nations.
To enable PostGIS on an existing PostgreSQL database, run:
CREATE EXTENSION postgis;
This single command registers the spatial types, functions, and indexes. You can verify the installation with SELECT PostGIS_Full_Version();.
Creating Spatial Tables
A spatial table is a regular PostgreSQL table with one or more geometry or geography columns. You define the column type using the PostGIS constructors:
CREATE TABLE cities (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
population INTEGER,
geom GEOMETRY(Point, 4326)
);
INSERT INTO cities (name, population, geom) VALUES
('New York', 8336817, ST_SetSRID(ST_MakePoint(-74.006, 40.7128), 4326)),
('London', 8982000, ST_SetSRID(ST_MakePoint(-0.1276, 51.5074), 4326)),
('Tokyo', 13960000, ST_SetSRID(ST_MakePoint(139.6917, 35.6895), 4326));
The GEOMETRY(Point, 4326) declaration constrains the column to point geometries in the WGS 84 coordinate reference system (EPSG:4326). PostGIS enforces this constraint, rejecting any insert that uses a different geometry type or SRID.
Geometry vs Geography
PostGIS offers two spatial types that differ in how they calculate distances and areas:
| Aspect | geometry | geography |
|---|---|---|
| Coordinate system | Planar (Cartesian) | Spheroidal (WGS 84) |
| Distance units | CRS units (degrees or meters) | Meters |
| Accuracy for large areas | Distorted at global scale | Accurate worldwide |
| Function support | 300+ functions | ~50 functions |
| Performance | Faster (simpler math) | Slower (geodesic calculations) |
| Best for | Local/regional analysis, projected data | Global queries, distance in meters |
Use geography when you need accurate distance and area calculations across large extents (e.g., "find all airports within 500 km"). Use geometry for projected data and local analysis where planar math is sufficient. You can cast between the two types with ::geography and ::geometry.
Importing Spatial Data
PostGIS supports multiple import methods depending on your source format:
- shp2pgsql — Bundled with PostGIS. Converts Shapefiles to SQL insert statements:
shp2pgsql -s 4326 roads.shp public.roads | psql -d mydb - ogr2ogr — Part of GDAL. Imports GeoJSON, KML, GeoPackage, CSV, and dozens of other formats:
ogr2ogr -f "PostgreSQL" PG:"dbname=mydb" input.geojson -nln my_table - COPY with WKT — For CSV files with WKT geometry columns, use PostgreSQL COPY and then cast the text to geometry.
- QGIS DB Manager — A graphical interface for dragging layers from QGIS directly into PostGIS tables.
For quick visualization of your data before importing, GeoDataTools lets you preview GeoJSON and KML files in the browser.
Essential Spatial Queries
The real power of PostGIS lies in spatial SQL. Below are the most commonly used spatial functions with practical examples. For the complete function reference, see the PostGIS documentation.
ST_Contains — Point-in-Polygon
ST_Contains(A, B) returns true if geometry A completely contains geometry B. This is the classic "which polygon does this point fall in?" query.
-- Find which country contains a given point
SELECT name FROM countries
WHERE ST_Contains(geom, ST_SetSRID(ST_MakePoint(-73.98, 40.75), 4326));
ST_Distance — Distance Calculation
ST_Distance(A, B) returns the minimum distance between two geometries. When used with geography types, the result is in meters.
-- Distance in meters between New York and London
SELECT ST_Distance(
ST_MakePoint(-74.006, 40.7128)::geography,
ST_MakePoint(-0.1276, 51.5074)::geography
) AS distance_meters;
ST_Buffer — Buffer Zone
ST_Buffer(geom, distance) creates a polygon representing all points within a given distance of the input geometry. Useful for proximity analysis and exclusion zones.
-- Create a 1 km buffer around each hospital (geography for meter units)
SELECT name, ST_Buffer(geom::geography, 1000)::geometry AS buffer_geom
FROM hospitals;
ST_Intersects — Overlap Detection
ST_Intersects(A, B) returns true if two geometries share any portion of space. It is the most commonly used spatial predicate for finding overlapping features.
-- Find all roads that cross a flood zone
SELECT roads.name FROM roads
JOIN flood_zones ON ST_Intersects(roads.geom, flood_zones.geom);
ST_Within and ST_DWithin
ST_Within(A, B) is the inverse of ST_Contains: it returns true if A is entirely inside B. ST_DWithin(A, B, distance) returns true if A and B are within a given distance, and is optimized to use spatial indexes for fast proximity searches.
-- Find all restaurants within 500 meters of a park
SELECT r.name FROM restaurants r, parks p
WHERE p.name = 'Central Park'
AND ST_DWithin(r.geom::geography, p.geom::geography, 500);
Spatial Indexing with GiST
Spatial queries without indexes perform full table scans, testing every row. On tables with millions of geometries, this is prohibitively slow. A GiST (Generalized Search Tree) index dramatically accelerates spatial predicates by building a bounding-box hierarchy:
CREATE INDEX idx_cities_geom ON cities USING GIST (geom);
Once created, PostGIS automatically uses the index for functions like ST_Contains, ST_Intersects, ST_DWithin, and bounding-box operators (&&). Always create a GiST index on every geometry column that will be used in WHERE clauses or JOIN conditions. The index adds modest overhead during inserts but provides orders-of-magnitude query speedups.
Exporting to GeoJSON with ST_AsGeoJSON
ST_AsGeoJSON converts a geometry into a GeoJSON geometry string, making it straightforward to serve spatial query results directly to web clients.
-- Export cities as a GeoJSON FeatureCollection
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(geom)::json,
'properties', json_build_object('name', name, 'population', population)
)
)
) AS geojson
FROM cities;
This pattern lets you build a REST API that returns GeoJSON directly from PostGIS, ready for consumption by Leaflet, OpenLayers, or GeoDataTools. For details on CRS considerations when exporting, see the understanding CRS in GIS guide.
FAQ
Do I need PostGIS for a simple web map?
Not necessarily. For small datasets (a few hundred features), loading a static GeoJSON file directly into a front-end mapping library is simpler and requires no database. PostGIS becomes valuable when you need to query millions of features, perform server-side spatial analysis, join spatial data with business data, or serve dynamic results via SQL.
What is the difference between ST_Intersects and ST_Contains?
ST_Intersects(A, B) returns true if A and B share any space at all — they can partially overlap, touch at a boundary, or one can be entirely inside the other. ST_Contains(A, B) is stricter: it returns true only if B is completely inside A with no part of B outside A.
How do I improve PostGIS query performance?
Three strategies have the biggest impact: (1) Create GiST indexes on all geometry columns used in spatial predicates. (2) Use ST_DWithin instead of computing ST_Distance and filtering, because ST_DWithin leverages the spatial index. (3) Simplify complex geometries with ST_Simplify when full precision is not needed, reducing the computational cost of intersection and containment tests.