Geospatial data with PostGIS
Maria Garcia
Feb 2026
2 tabs
-- Install PostGIS extension
CREATE EXTENSION IF NOT EXISTS postgis;
-- Create table with geometry column
CREATE TABLE locations (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
location GEOMETRY(Point, 4326), -- 4326 = WGS 84 (GPS coordinates)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create spatial index
CREATE INDEX idx_locations_geom ON locations USING GIST(location);
-- Insert point (longitude, latitude)
INSERT INTO locations (name, location) VALUES
('Statue of Liberty', ST_SetSRID(ST_MakePoint(-74.0445, 40.6892), 4326)),
('Empire State Building', ST_SetSRID(ST_MakePoint(-73.9857, 40.7484), 4326)),
('Central Park', ST_SetSRID(ST_MakePoint(-73.9654, 40.7829), 4326));
-- Insert from lat/lon columns
INSERT INTO locations (name, location)
SELECT
name,
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)
FROM raw_locations;
-- Find distance between two points (meters)
SELECT
l1.name AS from_location,
l2.name AS to_location,
ST_Distance(
l1.location::geography,
l2.location::geography
) AS distance_meters
FROM locations l1
CROSS JOIN locations l2
WHERE l1.id != l2.id;
-- Find locations within radius (5km)
SELECT
name,
ST_Distance(
location::geography,
ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)::geography
) AS distance_meters
FROM locations
WHERE ST_DWithin(
location::geography,
ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)::geography,
5000 -- 5000 meters = 5km
)
ORDER BY distance_meters;
-- Nearest neighbors (5 closest locations)
SELECT
name,
ST_Distance(
location::geography,
ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)::geography
) AS distance_meters
FROM locations
ORDER BY location <-> ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)
LIMIT 5;
-- Extract coordinates
SELECT
name,
ST_X(location) AS longitude,
ST_Y(location) AS latitude
FROM locations;
-- Polygon (service area, delivery zone)
CREATE TABLE delivery_zones (
id SERIAL PRIMARY KEY,
zone_name VARCHAR(100),
boundary GEOMETRY(Polygon, 4326)
);
-- Create polygon
INSERT INTO delivery_zones (zone_name, boundary) VALUES (
'Downtown',
ST_SetSRID(
ST_MakePolygon(
ST_GeomFromText('LINESTRING(
-74.02 40.70,
-74.00 40.70,
-74.00 40.72,
-74.02 40.72,
-74.02 40.70
)')
),
4326
)
);
-- Check if point is within polygon
SELECT
l.name,
dz.zone_name
FROM locations l
JOIN delivery_zones dz ON ST_Within(l.location, dz.boundary);
-- Find zone for a specific point
SELECT zone_name
FROM delivery_zones
WHERE ST_Contains(
boundary,
ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)
);
-- Bounding box query (faster preliminary filter)
SELECT name
FROM locations
WHERE location && ST_MakeEnvelope(
-74.05, 40.70, -- min lon, min lat
-73.95, 40.80, -- max lon, max lat
4326
);
-- LineString (routes, paths)
CREATE TABLE routes (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
path GEOMETRY(LineString, 4326)
);
INSERT INTO routes (name, path) VALUES (
'Broadway',
ST_SetSRID(
ST_MakeLine(ARRAY[
ST_MakePoint(-73.9857, 40.7484),
ST_MakePoint(-73.9851, 40.7580),
ST_MakePoint(-73.9845, 40.7676)
]),
4326
)
);
-- Length of path
SELECT
name,
ST_Length(path::geography) AS length_meters
FROM routes;
-- Intersection detection
SELECT
r1.name AS route1,
r2.name AS route2
FROM routes r1
JOIN routes r2 ON r1.id < r2.id
WHERE ST_Intersects(r1.path, r2.path);
-- Buffer zone around point (500m radius)
SELECT
name,
ST_Buffer(location::geography, 500)::geometry AS buffer_zone
FROM locations;
-- Centroid of polygon
SELECT
zone_name,
ST_Centroid(boundary) AS center_point,
ST_X(ST_Centroid(boundary)) AS center_lon,
ST_Y(ST_Centroid(boundary)) AS center_lat
FROM delivery_zones;
-- Area of polygon (square meters)
SELECT
zone_name,
ST_Area(boundary::geography) AS area_sqm,
ST_Area(boundary::geography) / 1000000 AS area_sqkm
FROM delivery_zones;
-- Convex hull (smallest polygon containing points)
SELECT
ST_ConvexHull(
ST_Collect(location)
) AS hull
FROM locations;
-- Closest point on line to a point
SELECT
name,
ST_ClosestPoint(
path,
ST_SetSRID(ST_MakePoint(-73.9850, 40.7500), 4326)
) AS closest_point_on_route
FROM routes;
-- Distance from point to line
SELECT
name,
ST_Distance(
path::geography,
ST_SetSRID(ST_MakePoint(-73.9850, 40.7500), 4326)::geography
) AS distance_to_route_meters
FROM routes;
-- Simplify geometry (reduce points for display)
SELECT
ST_Simplify(path, 0.001) AS simplified_path
FROM routes;
-- GeoJSON export
SELECT
jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(
jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(location)::jsonb,
'properties', jsonb_build_object(
'name', name,
'id', id
)
)
)
) AS geojson
FROM locations;
-- Spatial join (all locations in each zone)
SELECT
dz.zone_name,
COUNT(l.id) AS location_count,
STRING_AGG(l.name, ', ') AS locations
FROM delivery_zones dz
LEFT JOIN locations l ON ST_Within(l.location, dz.boundary)
GROUP BY dz.zone_name;
-- Heatmap data (count points in grid cells)
WITH grid AS (
SELECT
i,
j,
ST_MakeEnvelope(
-74.05 + (i * 0.01),
40.70 + (j * 0.01),
-74.05 + ((i + 1) * 0.01),
40.70 + ((j + 1) * 0.01),
4326
) AS cell
FROM generate_series(0, 9) AS i
CROSS JOIN generate_series(0, 9) AS j
)
SELECT
i,
j,
COUNT(l.id) AS point_count,
ST_AsText(cell) AS cell_bounds
FROM grid
LEFT JOIN locations l ON ST_Within(l.location, cell)
GROUP BY i, j, cell;
-- Spatial clustering (group nearby points)
SELECT
ST_ClusterKMeans(location::geometry, 3) OVER () AS cluster_id,
name,
location
FROM locations;
-- Transform between coordinate systems
-- Convert from WGS84 (4326) to Web Mercator (3857)
SELECT
name,
ST_Transform(location, 3857) AS web_mercator_location
FROM locations;
-- Validate geometry
SELECT
name,
ST_IsValid(boundary) AS is_valid,
ST_IsValidReason(boundary) AS validation_message
FROM delivery_zones;
2 files · sql
Explain with highlit
Geospatial data represents geographic locations and shapes. I use PostGIS for spatial queries. Point data stores coordinates—latitude, longitude. LineString represents paths. Polygon defines areas. Spatial indexes (GIST) enable fast proximity queries. Distance calculations find nearby points. Intersection queries detect overlaps. Containment checks if point is within polygon. Understanding spatial reference systems ensures accuracy. Bounding box queries filter before precise calculations. Geospatial aggregations compute centroids, unions. Essential for mapping applications, location services, delivery routing, geofencing. PostGIS rivals specialized GIS systems for many use cases.