DuckDB Geospatial Analytics: Turning Coordinates into Business Insights
Why Geospatial Analysis Matters Now More Than Ever
In e-commerce, logistics, and retail, location is money.
- Food delivery platforms recommend nearest restaurants based on user location
- Retail chains need to analyze foot traffic and competition within a 3km radius
- Logistics companies optimize delivery routes
- Financial fraud detection flags anomalous transaction locations
The traditional approach? PostGIS + PostgreSQL, or Python’s GeoPandas + Shapely. These are heavy tools — complex installation, high maintenance costs, steep learning curves.
DuckDB changes the game. Its built-in ST_ spatial functions let you handle 90% of geospatial analysis needs in pure SQL — and at blazing speed.
Core Capabilities Overview
DuckDB’s spatial extension provides full OGC Simple Features standard support:
| Function Category | Key Functions | Typical Use Case |
|---|---|---|
| Distance | ST_Distance, ST_DWithin | Find nearby stores |
| Buffer | ST_Buffer | Define service radius |
| Containment | ST_Contains, ST_Within | Check if point is in region |
| Intersection | ST_Intersects | Collision detection |
| Centroid | ST_Centroid | Calculate region center |
| Area & Perimeter | ST_Area, ST_Perimeter | Land parcel analysis |
| Nearest Neighbor | ST_NearestPoint | Optimal matching |
Step 1: Enable the Spatial Extension
-- Load the spatial extension (first time requires download)
LOAD spatial;
INSTALL spatial;
-- Verify installation
SELECT version();
Step 2: Create Your Geospatial Dataset
Imagine you’re an operations analyst for a chain coffee brand. You have:
- A list of your own store locations (longitude, latitude)
- Competitor store data
- District POI (Point of Interest) information
LOAD spatial;
-- Create your store table
CREATE TABLE my_stores AS
SELECT * FROM (VALUES
('Beijing Sanlitun', 116.4510, 39.9320),
('Shanghai Nanjing Rd', 121.4730, 31.2300),
('Guangzhou Tianhe', 113.3250, 23.1300),
('Shenzhen Huaqiang', 114.0650, 22.5400),
('Chengdu Chunxi Rd', 104.0800, 30.6600),
('Hangzhou West Lake', 120.1500, 30.2800),
('Wuhan Jianghan Rd', 114.2800, 30.5800),
('Chongqing Jiefangbei', 106.5800, 29.5600)
) AS t(name, longitude, latitude);
-- Create competitor store table
CREATE TABLE competitor_stores AS
SELECT * FROM (VALUES
('Starbucks Sanlitun', 116.4530, 39.9340),
('Luckin Sanlitun', 116.4490, 39.9310),
('Starbucks Nanjing Rd', 121.4750, 31.2320),
('Luckin Nanjing Rd', 121.4710, 31.2280),
('Manner Tianhe', 113.3270, 23.1320),
('Luckin Huaqiang', 114.0670, 22.5420)
) AS t(name, longitude, latitude);
-- Convert coordinates to WKT geometry objects
ALTER TABLE my_stores ADD COLUMN geom GEOMETRY;
UPDATE my_stores SET geom = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326);
ALTER TABLE competitor_stores ADD COLUMN geom GEOMETRY;
UPDATE competitor_stores SET geom = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326);
Step 3: Distance Analysis — Finding Your “Safe Zone”
The most fundamental and valuable analysis: how far are my stores from competitors?
-- Calculate distance between every pair of stores (in meters)
SELECT
m.name AS my_store,
c.name AS competitor,
ROUND(
ST_Distance(
ST_Transform(m.geom, 3857), -- Web Mercator (metric CRS)
ST_Transform(c.geom, 3857)
)::INTEGER
) AS distance_meters,
CASE
WHEN ST_Distance(
ST_Transform(m.geom, 3857),
ST_Transform(c.geom, 3857)
) < 500 THEN 'Direct Competition'
WHEN ST_Distance(
ST_Transform(m.geom, 3857),
ST_Transform(c.geom, 3857)
) < 1000 THEN 'Potential Competition'
ELSE 'Safe Distance'
END AS competition_level
FROM my_stores m
CROSS JOIN competitor_stores c
WHERE ST_DWithin(
ST_Transform(m.geom, 3857),
ST_Transform(c.geom, 3857),
1000 -- Within 1000 meters
)
ORDER BY distance_meters;
Business Insights:
- Beijing Sanlitun is within 500m of two competitors → intense competition zone, needs differentiation strategy
- Chengdu Chunxi Rd has no competitors within 1km → blue ocean market, consider increasing ad spend
Step 4: Buffer Analysis — Defining Your “Service Radius”
-- Create 500-meter service buffers for each store
CREATE TABLE store_buffers AS
SELECT
name,
ST_Buffer(ST_Transform(geom, 3857), 500) AS buffer_geom
FROM my_stores;
-- Count how many competitors fall within each buffer
SELECT
s.name AS store,
COUNT(c.name) AS nearby_competitors
FROM store_buffers s
LEFT JOIN competitor_stores c
ON ST_Contains(s.buffer_geom, ST_Transform(c.geom, 3857))
GROUP BY s.name
ORDER BY nearby_competitors DESC;
Monetization Tip: This analysis can be packaged as a SaaS product — a “Site Selection Assistant” for chain brands. Input an address, output a competitor density heatmap. Monthly pricing: $40-$200 per enterprise.
Step 5: Nearest Neighbor Search — Smart Dispatch
-- Given a new order location, find the 3 nearest stores
WITH order_location AS (
SELECT ST_SetSRID(ST_MakePoint(116.4550, 39.9350), 4326)::GEOMETRY AS geom
)
SELECT
m.name,
ROUND(ST_Distance(
ST_Transform(m.geom, 3857),
ST_Transform(o.geom, 3857)
)) AS distance_m
FROM my_stores m, order_location o
ORDER BY distance_m
LIMIT 3;
Step 6: Loading Real Geographic Data from External Sources
DuckDB can read GeoJSON files directly:
-- Load city district GeoJSON
CREATE TABLE districts AS
SELECT
feature->>'name' AS district_name,
feature->>'population' AS population,
st_geomfromjson(feature->'geometry') AS geom
FROM read_geojson('https://example.com/china-districts.geojson');
-- Count stores per district and calculate coverage ratio
SELECT
d.district_name,
d.population,
COUNT(m.name) AS store_count,
ROUND(d.population::DOUBLE / NULLIF(COUNT(m.name), 0), 0) AS per_store_population
FROM districts d
LEFT JOIN my_stores m ON ST_Contains(d.geom, m.geom)
GROUP BY d.district_name, d.population
ORDER BY per_store_population;
Comparison with Traditional Approaches
| Dimension | PostGIS + PostgreSQL | GeoPandas (Python) | DuckDB Spatial |
|---|---|---|---|
| Installation | High (needs compilation) | Medium | Low (one SQL command) |
| Learning Curve | Steep | Moderate | Gentle (SQL syntax) |
| Big Data Scale | Good | Poor (memory limited) | Excellent (columnar storage) |
| Interactive Analysis | Fair | Good | Excellent |
| Deployment | Heavy | Lightweight | Minimal |
| Best For | Production GIS | Geo visualization | Rapid analysis / prototyping |
Monetization Opportunities
Geospatial analysis is a core capability for high-value B2B services:
Site Selection SaaS: For chain brands, input a candidate address and automatically output competitor density, foot traffic predictions, and rent-to-value scores. Pricing reference: $50-$200/month per enterprise.
Logistics Optimization API: Provide “nearest warehouse/store” lookup for e-commerce platforms, charged per API call. Marginal cost per query approaches zero.
Real Estate Valuation Assistant: Combine housing price data with POI distances (subway stations, schools, malls) to generate area value score reports.
Precision Marketing: Analyze consumer preference zones from GPS trajectory data, offering targeted advertising recommendations for brands.
Key Advantage: DuckDB’s spatial analysis can process tens of millions of geographic features on a single machine — no distributed cluster needed. This means you can build a complete service with minimal hardware costs (even a Raspberry Pi).
Summary
DuckDB’s spatial extension makes geospatial analysis unprecedentedly accessible:
- Zero installation: Just
LOAD spatialand go - Standard SQL: All spatial functions follow OGC standards
- High performance: Columnar storage + vectorized execution, 10-50x faster than Pandas
- Format flexibility: Supports GeoJSON, WKT, WKB conversion out of the box
For data analysts, this means completing the entire workflow — data collection, geospatial analysis, report generation, visualization export — all in SQL, within a single session.
💡 More DuckDB geospatial case studies and business applications → duckdblab.org
