Load GeoPackage into Postgres with ogr_fdw
Use the ogr_fdw foreign-data wrapper to expose a Land Records .gpkg as a Postgres foreign table, then materialize selected rows into PostGIS for indexed queries.
ogr_fdw lets Postgres read any GDAL-supported data source — including GeoPackage — through GDAL's OGR layer. It is the lightest-weight way to get a Land Records .gpkg into Postgres without converting the file or pre-loading every row. You can also use it as a one-shot loader to materialize the data into a native PostGIS table.
Prerequisites
- PostgreSQL 14+
- PostGIS 3.x
ogr_fdw(github) compiled against the same Postgres version. On Debian/Ubuntu:apt install postgresql-17-ogr-fdw.- The
.gpkgfile on the database server's local filesystem, or on a path the Postgres process can read.
1. Install the extensions
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS ogr_fdw;2. Create the foreign server
Point the server at the absolute path of the .gpkg:
CREATE SERVER landrecords_gpkg
FOREIGN DATA WRAPPER ogr_fdw
OPTIONS (
datasource '/srv/data/parcels-2026.2.gpkg',
format 'GPKG'
);If Postgres can't see your file, SHOW data_directory; and confirm the path is readable by the OS user the Postgres process runs as (typically postgres).
3. Import the layer as a foreign schema
This creates one foreign table per layer in the .gpkg. Land Records files contain a single lr_parcel_us layer:
CREATE SCHEMA IF NOT EXISTS landrecords_fdw;
IMPORT FOREIGN SCHEMA ogr_all
FROM SERVER landrecords_gpkg
INTO landrecords_fdw;The geometry column comes through as PostGIS geometry(MultiPolygon, 4326). Sanity-check:
\d landrecords_fdw.lr_parcel_us
SELECT count(*) FROM landrecords_fdw.lr_parcel_us;4. Query directly (no copy)
Reading via FDW round-trips through GDAL on every query, so each WHERE clause re-scans the GeoPackage. Use it for ad-hoc lookups:
SELECT lrid, ownername, parceladdr
FROM landrecords_fdw.lr_parcel_us
WHERE statefp = '01' AND countyfp = '003'
LIMIT 10;For anything you'll query repeatedly, materialize.
5. Materialize into PostGIS (recommended for production)
A single CTAS pulls the whole layer (or a slice) into a real Postgres table where indexes, joins, and concurrency all work properly:
-- One state, ~2M rows, ~3 minutes on commodity hardware.
CREATE TABLE landrecords.al_parcels AS
SELECT *
FROM landrecords_fdw.lr_parcel_us
WHERE statefp = '01';
CREATE INDEX al_parcels_geom_gix
ON landrecords.al_parcels USING GIST (geom);
CREATE INDEX al_parcels_statefp_countyfp_idx
ON landrecords.al_parcels (statefp, countyfp);
ANALYZE landrecords.al_parcels;For the full nationwide dataset (~160M rows), partition by statefp and load one state at a time:
CREATE TABLE landrecords.parcels (
LIKE landrecords_fdw.lr_parcel_us INCLUDING ALL
) PARTITION BY LIST (statefp);
-- For each state FIPS code:
DO $$
DECLARE fp text;
BEGIN
FOR fp IN
SELECT DISTINCT statefp FROM landrecords_fdw.lr_parcel_us
LOOP
EXECUTE format(
'CREATE TABLE landrecords.parcels_%1$s ' ||
'PARTITION OF landrecords.parcels FOR VALUES IN (%2$L)',
fp, fp
);
EXECUTE format(
'INSERT INTO landrecords.parcels SELECT * FROM landrecords_fdw.lr_parcel_us WHERE statefp = %1$L',
fp
);
RAISE NOTICE 'loaded state %', fp;
END LOOP;
END $$;
CREATE INDEX parcels_geom_gix ON landrecords.parcels USING GIST (geom);
ANALYZE landrecords.parcels;6. Refreshing on each quarterly release
When you download the next quarter's .gpkg, simply repoint the server and re-materialize:
ALTER SERVER landrecords_gpkg
OPTIONS (SET datasource '/srv/data/parcels-2026.3.gpkg');
TRUNCATE landrecords.al_parcels;
INSERT INTO landrecords.al_parcels
SELECT * FROM landrecords_fdw.lr_parcel_us
WHERE statefp = '01';Troubleshooting
could not open data source— the path is wrong, or the Postgres OS user can't read the file. Trysudo -u postgres cat /srv/data/parcels-2026.2.gpkg > /dev/nullto confirm.type geometry does not exist— PostGIS isn't installed in the schema you're importing into. Create it:CREATE EXTENSION postgis;.OGR error: Invalid integer field width— usually a staleogr_fdwbuild against a newer GDAL. Rebuild or upgrade the package.- Foreign table queries are slow — that's expected; materialize into a native table (step 5).
Read GeoPackage with GeoPandas
Load a Land Records .gpkg file into a GeoPandas GeoDataFrame, inspect attributes, and run typical filter / area / plotting workflows.
Read GeoPackage with DuckDB
Query a Land Records .gpkg file directly with DuckDB's spatial extension — no import step, no separate database.