Week 6 · Orbital Analyst

PostGIS: spatial SQL fundamentals

PostGIS turns PostgreSQL into a full GIS engine. Spatial data types, ST_ functions, GIST indexes — the toolkit for serious geospatial backends.

Learning objectives

Primer

PostGIS is the spatial extension to PostgreSQL. It turns the world's most-loved relational database into a full-featured GIS engine: spatial data types, hundreds of ST_* functions, GIST indexes for sub-millisecond spatial queries, and the ability to combine all of that with the regular relational and JSON capabilities you already use Postgres for.

For any serious space-GIS backend — including LaunchDetect's production pipeline — PostGIS is the default. This week is your fluency primer.

Installation

The easiest path is Docker:

docker run -d --name pg-spatial \
  -e POSTGRES_PASSWORD=academy \
  -p 5432:5432 \
  postgis/postgis:16-3.4

Then enable PostGIS in your database:

CREATE EXTENSION IF NOT EXISTS postgis;
SELECT PostGIS_Version();  -- confirms installation

Spatial data types

PostGIS adds three core types: GEOMETRY (planar), GEOGRAPHY (geodesic), and the array variants. The decision between geometry and geography is the most important one in your schema:

Define a launch detections table:

CREATE TABLE detections (
  id           bigserial PRIMARY KEY,
  detected_at  timestamptz NOT NULL,
  vehicle      text,
  brightness_k real,
  -- WGS84 geographic; SRID 4326 = lat/lon on WGS84
  position     geometry(Point, 4326) NOT NULL
);
CREATE INDEX detections_position_gix ON detections USING GIST (position);

Loading data

Load a GeoJSON FeatureCollection via ogr2ogr (from GDAL):

ogr2ogr -f PostgreSQL "PG:host=localhost user=postgres dbname=academy" \
  detections.geojson -nln detections -append

Or via Python with geopandas:

import geopandas as gpd
from sqlalchemy import create_engine

gdf = gpd.read_file('detections.geojson')
engine = create_engine('postgresql://postgres:academy@localhost:5432/academy')
gdf.to_postgis('detections', engine, if_exists='replace', index=False)

The ST_* family

The functions you'll use 90% of the time:

GIST indexes: the secret to speed

Without a spatial index, every spatial query is a full table scan. With a GIST index, PostgreSQL uses an R-tree to prune to candidate features in O(log n) time. Always create a GIST index on the geometry column. Always.

The lab

You'll load the Natural Earth global coastline polygons table and the LaunchDetect launches table into PostGIS, then write the spatial SQL query that finds every detection within 100 km of any coastline, sorted by distance. This single query is the core of LaunchDetect's coastal-spaceport heuristic for ranking detection confidence (a thermal hotspot 500 km from any coast is more likely a wildfire than a launch).

Hands-on lab: Find every launch within 100 km of a coastline

Load a global coastlines table and a launch-detection points table into PostGIS. Write the spatial SQL query that returns all launches within 100 km of any coastline, sorted by distance.

Quiz

Test yourself. Answer key on the certificate-track page (Gold-tier feature: progress tracking and auto-grading).

Q1. PostGIS adds what to PostgreSQL?
  1. JSON support
  2. Spatial data types and functions
  3. Time series
  4. Web hosting
Q2. ST_Distance returns distance in:
  1. Always meters
  2. Always degrees
  3. The units of the input geometry's SRID
  4. Kilometers
Q3. GIST index is used for:
  1. Numeric columns
  2. Spatial columns
  3. Text search
  4. Sequence generation
Q4. ST_Within(a, b) returns true when:
  1. a is fully inside b
  2. a touches b
  3. a equals b
  4. a is north of b
Q5. SRID 4326 means:
  1. WGS84 lat/lon
  2. Web Mercator
  3. UTM Zone 26
  4. OSGB36