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
elevationandregionreads 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 = 2024can 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
geometadata 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
geoblock 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
- GeoPackage vs PostGIS
- FlatGeobuf for Fast Spatial Exchange
- Metadata Every GIS Dataset Needs
- Spatial data products