Short answer

GeoParquet is a specification that stores vector geometry inside Apache Parquet, the open columnar file format used across modern data engineering. It is not a replacement for GeoPackage on the desktop; it is a format for analytical workloads — scanning, filtering, and aggregating tables of millions to billions of features in tools like DuckDB, Apache Sedona, GeoPandas, BigQuery, and Spark.

If your work is "open a layer, edit a feature, save," GeoParquet is the wrong tool. If your work is "compute zonal statistics over 40 million parcels," or "join a continental point cloud of observations to administrative regions," GeoParquet is often dramatically faster and cheaper than row-based formats.

Why columnar changes everything for analytics

A shapefile, GeoJSON, or GeoPackage feature table is row-oriented: all fields of feature 1, then all fields of feature 2, and so on. To compute the mean of one numeric column over ten million rows, the engine still has to read every row's full geometry and every other attribute off disk.

Parquet is column-oriented: each column is stored contiguously and compressed independently. Three properties follow directly:

  • Column pruning. A query touching only elevation and region reads just those two columns and never loads the (usually large) geometry blob. On a wide table this alone can cut I/O by an order of magnitude.
  • Predicate pushdown via statistics. Parquet splits a file into row groups, and each row group stores min/max statistics per column. An engine filtering WHERE year = 2024 can skip entire row groups whose max year is 2023 without decompressing them.
  • Efficient encoding and compression. Dictionary encoding, run-length encoding, and codecs like Snappy or ZSTD shrink columnar data far more than row data, because adjacent values in a column are similar.

The GeoParquet specification (currently in the 1.x line, stewarded under OGC) adds a geo key to the Parquet file's key-value metadata. That block records the geometry column name, encoding (WKB by default, with native GeoArrow encoding gaining adoption), and — critically — the CRS as a PROJJSON object. Storing the CRS in metadata is a real improvement over the shapefile's separate .prj file that so often goes missing.

How geometry is stored

In GeoParquet 1.0, geometry sits in a binary column as Well-Known Binary (WKB), with the geo metadata declaring which column is the primary geometry, its geometry types, and a bounding box (bbox) covering the dataset. Newer tooling can also write GeoArrow encoding, which stores coordinates as native nested arrays rather than opaque WKB, enabling vectorized geometry operations without a parse step. When you can, also write a per-row bounding-box column or rely on row-group bbox covering so spatial engines can prune by extent.

Worked example: reading and writing

GDAL / ogr2ogr

GDAL has a Parquet driver from version 3.5 onward (built against the Arrow/Parquet libraries). Convert a GeoPackage to GeoParquet:

ogr2ogr -f Parquet observations.parquet observations.gpkg samples \
  -lco COMPRESSION=ZSTD \
  -lco ROW_GROUP_SIZE=100000 \
  -lco GEOMETRY_ENCODING=WKB

ROW_GROUP_SIZE is a tuning knob: larger row groups improve compression but coarsen the granularity of statistics-based skipping. For wide analytical tables, 50k–200k rows per group is a reasonable starting band. Inspect the result with ogrinfo -al -so observations.parquet.

GeoPandas

import geopandas as gpd

gdf = gpd.read_parquet("observations.parquet")
# columnar read pulls only what you touch downstream
north = gdf[gdf["region"] == "north"]
north.to_parquet("north.parquet", compression="zstd")

DuckDB

DuckDB's spatial extension reads GeoParquet directly and pushes filters down, which makes it a strong fit for ad-hoc analytics on a laptop:

INSTALL spatial; LOAD spatial;
SELECT region, count(*) AS n, avg(elevation) AS mean_elev
FROM read_parquet('observations.parquet')
WHERE year = 2024
GROUP BY region;

Because the engine prunes the geometry column and skips 2023 row groups via statistics, this aggregate scans a fraction of the file.

Partitioning for scale

For truly large datasets, write partitioned (Hive-style) Parquet directories — for example partitioned by year and region — so engines can skip whole directories before touching any file:

observations/
  year=2023/region=north/part-0.parquet
  year=2024/region=north/part-0.parquet
  year=2024/region=south/part-0.parquet

A query filtering year=2024 AND region='north' then reads exactly one file. This is how GeoParquet underpins cloud-native analytics on object storage (S3, GCS): combine partition pruning, row-group skipping, and column pruning, and you read kilobytes instead of gigabytes.

When not to use GeoParquet

  • Interactive editing. Parquet files are immutable; you rewrite, not update in place. For "edit one feature and save," use GeoPackage or PostGIS.
  • Tiny datasets. With a few thousand features the columnar overhead and the lack of native editing outweigh the benefits.
  • Desktop interchange with older tools. Some GIS installations lack the Arrow/Parquet driver. Confirm the recipient's GDAL is 3.5+ or hand them a GeoPackage.
  • Single-feature random access. Parquet excels at scans, not point lookups; a spatially indexed database is better for "give me this one feature by id."

Common pitfalls and why they happen

  • CRS lost on round-trip through plain Parquet tooling. Generic Parquet writers that ignore the geo metadata drop the CRS and geometry declaration, leaving a table that other tools read as opaque bytes. Always use a GeoParquet-aware writer.
  • One giant row group. Writing the whole table as a single row group disables statistics-based skipping, so every query scans everything. Set a sensible ROW_GROUP_SIZE.
  • Mixing geometry types without declaring them. Engines that rely on the declared geometry types in metadata can misbehave; keep the geo block accurate.
  • Expecting concurrent writes. Parquet has no transactions. Coordinate writes at the orchestration layer, or use a table format (Iceberg/Delta) on top.

QA and validation

Validate that the geo metadata block exists and names the correct CRS (PROJJSON should match your expected EPSG code), confirm feature counts before and after conversion, and run a spatial sanity check — read the file back and compare a known geometry's coordinates and the overall bounding box. For partitioned datasets, verify that partition columns are not also duplicated inside the files unless intended.

Bathyl perspective

We reach for GeoParquet when a dataset graduates from "a layer someone opens" to "a table an engine queries." Authored data and editing stay in PostGIS or GeoPackage; the analytical mirror lands as partitioned GeoParquet on object storage, where DuckDB or Sedona can scan it cheaply. The format is a performance decision tied to query patterns and data volume, not a default.

Related reading

Sources