Loading the Data

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 .gpkg file 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.

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. Try sudo -u postgres cat /srv/data/parcels-2026.2.gpkg > /dev/null to 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 stale ogr_fdw build against a newer GDAL. Rebuild or upgrade the package.
  • Foreign table queries are slow — that's expected; materialize into a native table (step 5).

On this page