DuckDB
GeoParquet
SQL
Analytics
Performance
Featured

Query GeoParquet with DuckDB: Geometry Types, CRS, and Spatial SQL

A native DuckDB workflow for GeoParquet: inspect WKB versus GEOMETRY, validate metadata, filter by extent, calculate in correct units, and verify query plans.

By 10 min read

DuckDB can query Parquet attributes directly and, in a native build with the spatial extension, decode and analyze geometry in the same SQL pipeline. The safe workflow is: identify the file’s GeoParquet version, inspect the exposed geometry type, load spatial functions only where supported, make the CRS and units explicit, and verify the query plan before claiming a speedup.

This tutorial targets native DuckDB and the current 1.5 documentation. viewparquet’s shipped browser workbench does not currently load the spatial extension; use it for table-level GeoParquet inspection, then move spatial SQL to a pinned native environment.

Pin the environment

Start a DuckDB CLI or native client and record what actually ran:

query.sql
SELECT version() AS duckdb_version;
 
INSTALL spatial;
LOAD spatial;
 
SELECT duckdb_proj_version() AS proj_version;

INSTALL may download the extension the first time. In a reproducible build, pin the DuckDB client and extension-compatible version in your lockfile or container. The GEOMETRY type moved into core DuckDB in v1.5, but most analytical geometry functions remain in the spatial extension; the DuckDB geometry type documentation (opens in a new tab) explains that boundary.

Use a controlled upstream file first

Download the GeoParquet project’s official five-row example (opens in a new tab) as example.parquet. viewparquet’s catalog fully verified the file on June 12, 2026 with five rows and six columns, then performed a lighter boundary probe on August 19.

query.sql
SELECT count(*) AS rows
FROM read_parquet('example.parquet');
 
DESCRIBE SELECT *
FROM read_parquet('example.parquet');

The expected row count for that verified file is:

rows
5

The relevant columns are pop_est, continent, name, iso_a3, gdp_md_est, and geometry. If today’s download differs, stop and record the new object hash rather than forcing the old expected result.

Branch on the geometry type

The upstream example is useful precisely because it exposes a compatibility question. Older GeoParquet files commonly present WKB as BLOB. A reader with native Parquet geospatial logical-type support may present a newer file as GEOMETRY or GEOGRAPHY.

If DESCRIBE says BLOB, decode WKB:

query.sql
WITH features AS (
  SELECT
    name,
    pop_est,
    ST_GeomFromWKB(geometry) AS geom
  FROM read_parquet('example.parquet')
)
SELECT
  name,
  ST_GeometryType(geom) AS geometry_type,
  ST_IsValid(geom) AS is_valid
FROM features
ORDER BY name;

If DESCRIBE already says GEOMETRY, use the column directly:

query.sql
SELECT
  name,
  ST_GeometryType(geometry) AS geometry_type,
  ST_IsValid(geometry) AS is_valid
FROM read_parquet('native-geometry.parquet')
ORDER BY name;

Applying ST_GeomFromWKB to an already decoded GEOMETRY is not a future-proof compatibility strategy. Keep the conversion in a version-specific view or ingestion adapter.

Confirm metadata separately

A geometry function succeeding does not prove GeoParquet compliance:

query.sql
SELECT *
FROM parquet_kv_metadata('example.parquet')
WHERE key = 'geo';
 
SELECT
  file_name,
  name,
  type,
  logical_type
FROM parquet_schema('example.parquet')
WHERE name IS NOT NULL
ORDER BY name;

Run gpq validate example.parquet or another GeoParquet-aware validator as described in the versioned GeoParquet guide. SQL checks and specification validation answer different questions.

Filter with an explicit envelope

For the WKB-style sample:

query.sql
WITH features AS (
  SELECT
    name,
    ST_GeomFromWKB(geometry) AS geom
  FROM read_parquet('example.parquet')
)
SELECT name
FROM features
WHERE ST_Intersects(
  geom,
  ST_MakeEnvelope(-20, -40, 60, 40)
)
ORDER BY name;

The envelope arguments are min_x, min_y, max_x, max_y. This query assumes the geometry coordinates are longitude/latitude in the same CRS as the numeric envelope. Confirm that assumption from the geo metadata; do not infer it from plausible-looking values.

For GeoParquet 2.0-style native geometry, a bounding-box predicate can be written with the extent operator:

query.sql
SELECT name
FROM read_parquet('native-geometry.parquet')
WHERE geometry && ST_MakeEnvelope(-20, -40, 60, 40);

Current DuckDB documentation says the && operator can use geometry statistics when available. Whether it prunes row groups depends on the file, statistics, spatial ordering, storage version, and optimizer.

Make distance and area units explicit

ST_Distance, ST_Length, and ST_Area are planar: their units are the coordinate units of the input. A result over longitude/latitude coordinates is in degrees or square degrees, not meters.

For an old WKB column known from validated metadata to be OGC:CRS84, project before calculating area:

query.sql
WITH features AS (
  SELECT
    name,
    ST_GeomFromWKB(geometry) AS geom
  FROM read_parquet('example.parquet')
)
SELECT
  name,
  ST_Area(
    ST_Transform(geom, 'OGC:CRS84', 'EPSG:6933', true)
  ) / 1000000.0 AS area_sq_km
FROM features
ORDER BY area_sq_km DESC;

The final true requests traditional x/y axis order for the transform. EPSG:6933 is an equal-area global projection, useful for this demonstration; choose a CRS appropriate to the region and accuracy requirements of your real analysis.

DuckDB also provides spheroid functions for some types. Their documentation specifies latitude, longitude input order for POINT_2D and meter output:

query.sql
SELECT ST_DWithin_Spheroid(
  ST_Point2D(43.7384, 7.4246),
  ST_Point2D(43.7300, 7.4200),
  1000
) AS within_one_kilometre;

That axis order differs from typical GeoJSON and GeoParquet WKB x/y order. Convert deliberately. Never relabel ST_DWithin over unprojected longitude/latitude as meters.

Use the aggregate function for many geometries

ST_Union is a two-geometry scalar function. To union a column, use the aggregate:

query.sql
WITH features AS (
  SELECT ST_GeomFromWKB(geometry) AS geom
  FROM read_parquet('example.parquet')
)
SELECT ST_Union_Agg(geom) AS merged
FROM features;

For a polygon coverage whose edges are known to match, review ST_CoverageUnion_Agg; for arbitrary inputs, do not substitute it without validating the coverage. Unions can be CPU- and memory-intensive even when the Parquet scan is selective.

A spatial join with a cheap prefilter

Use an extent test before an exact predicate when it preserves correctness:

query.sql
SELECT
  p.place_id,
  a.area_id
FROM read_parquet('places.parquet') p
JOIN read_parquet('areas.parquet') a
  ON p.geometry && a.geometry
 AND ST_Within(p.geometry, a.geometry);

This example assumes both files expose native GEOMETRY values in the same CRS. For WKB/BLOB inputs, decode in a view first. For different CRSs, transform one side before joining.

Verify performance with the plan

Do not call a query “high-performance” because it uses DuckDB or GeoParquet. Measure it:

query.sql
EXPLAIN ANALYZE
SELECT count(*)
FROM read_parquet('native-geometry.parquet')
WHERE geometry && ST_MakeEnvelope(-20, -40, 60, 40);

Record:

  • DuckDB and spatial extension versions;
  • input URL/hash and GeoParquet version;
  • row groups, row count, and compressed bytes;
  • whether geometry statistics exist;
  • spatial ordering method;
  • cold and warm runs;
  • selected columns and predicate;
  • hardware and storage path;
  • rows returned and plan text.

Compare with a full count and an attribute-only predicate. A remote wildcard without a dated snapshot is not a reproducible benchmark because both files and schema can change.

Browser versus native DuckDB

Capabilityviewparquet todayNative DuckDB + spatial
Open Parquet and inspect attributesYes, subject to browser supportYes
Show schema and raw geometry valuesYesYes
Validate GeoParquet specificationNo; use GPQ/geoparquet-ioExternal validator
Load spatial extensionNot in the shipped workbenchYes
ST_Intersects, transforms, spatial joinsNot promisedYes, with spatial
Render a mapNoExport to a mapping layer

Use the browser to inspect a file without setting up Python or a GIS desktop, not as a substitute for a pinned native spatial test. For delivery to a web map, continue with the MapLibre and PMTiles guide.

Primary references