The short answer
The reliable way to load a shapefile into PostGIS is ogr2ogr when you want reprojection and scripting, or shp2pgsql when you want an inspectable SQL dump. Either way the three things that decide whether the import is correct are: getting the SRID right (and knowing whether you are assigning or reprojecting it), getting the character encoding right (so accented attribute values survive), and promoting geometries to MULTI so mixed single/multi parts do not fail the load. Everything after that — indexing, validation, automation — is straightforward.
Why the loader choice matters
A shapefile is really a set of files: .shp (geometry), .dbf (attributes), .shx (index), and ideally .prj (CRS as WKT) and .cpg (encoding). PostGIS stores geometry in a typed column with an SRID, backed by PostgreSQL's transactional engine and GiST spatial indexing. The loader's job is to map the shapefile's CRS, schema, and encoding into that model faithfully. Both standard tools do this well; they differ in ergonomics.
Importing with ogr2ogr
ogr2ogr (from GDAL) is the most flexible and the one to reach for in pipelines:
ogr2ogr -f PostgreSQL \
PG:"host=localhost dbname=gis user=gisuser" \
parcels.shp \
-nln parcels \
-nlt PROMOTE_TO_MULTI \
-lco GEOMETRY_NAME=geom \
-lco FID=id \
-lco SPATIAL_INDEX=GIST \
-t_srs EPSG:2154
What each part does:
-nln parcels— names the target table.-nlt PROMOTE_TO_MULTI— coerces a layer that mixesPOLYGONandMULTIPOLYGONintoMULTIPOLYGONso the load does not abort on type conflict (the single most common ogr2ogr failure).-lco GEOMETRY_NAME=geom— names the geometry columngeomrather than the defaultwkb_geometry.-lco FID=id— names the primary key column.-lco SPATIAL_INDEX=GIST— builds the spatial index at load time.-t_srs EPSG:2154— reprojects to Lambert-93 during load. If the data is already in the right CRS and the.prjis correct, omit this. If the.prjis missing or wrong but the coordinates are already correct, use-a_srs EPSG:2154to assign without moving coordinates, or-s_srsto declare a wrong-labelled source before reprojecting.
Useful extras: -overwrite to replace an existing table, -append to add to one, -progress for feedback, and --config PG_USE_COPY YES to load via COPY for a large speed gain on big files.
Importing with shp2pgsql
shp2pgsql ships with PostGIS and emits SQL, which you pipe to psql:
shp2pgsql -s 2154 -I -D -W LATIN1 \
parcels.shp public.parcels | psql -d gis
Flags:
-s 2154— sets the SRID. Use-s 4326:2154to reproject from one to another.-I— creates a GiST spatial index after load.-D— uses the PostgreSQL dump format (faster than INSERT statements).-W LATIN1— declares the source encoding so accented text converts correctly to the database's UTF-8.-aappends,-ddrops and recreates,-pproduces schema only.
Because it produces plain SQL, shp2pgsql -p is handy for previewing the exact table definition before committing.
Getting the SRID right
This is where silently-wrong results come from. Two distinct operations:
- Assign SRID (
-a_srsin ogr2ogr; matching-svalue in shp2pgsql) — tags the geometry with an SRID without changing coordinates. Correct when the numbers are already in that CRS. In SQL after the fact:UPDATE parcels SET geom = ST_SetSRID(geom, 2154);— but noteST_SetSRIDonly relabels, it does not move geometry. - Reproject (
-t_srsin ogr2ogr;-s FROM:TOin shp2pgsql) — recomputes coordinates into a new CRS. In SQL:ST_Transform(geom, 2154). Use when the coordinates are genuinely in another CRS.
Confusing ST_SetSRID (relabel) with ST_Transform (recompute) is the classic PostGIS error that produces data which looks fine in metadata but sits in the wrong place. Always confirm the .prj exists and is correct before trusting an automatic SRID — a missing .prj leaves ogr2ogr loading geometry as SRID 0.
Fixing encoding
Garbled accents mean the shapefile's .dbf encoding did not match the loader's assumption. The .cpg file, if present, names the encoding; if absent, legacy shapefiles are often LATIN1, ISO-8859-1, or a Windows codepage like CP1252. With ogr2ogr, set it via the environment or open option:
PGCLIENTENCODING=UTF8 ogr2ogr ... --config SHAPE_ENCODING "ISO-8859-1" ...
With shp2pgsql, use -W as shown. The target database should be UTF-8; the loader transcodes from the declared source encoding into it.
Validate the import
Never assume success because a table appeared. Check:
-- row count vs the source feature count from ogrinfo -al -so
SELECT count(*) FROM parcels;
-- SRID is what you intended, and consistent
SELECT DISTINCT ST_SRID(geom) FROM parcels;
-- geometry type and validity
SELECT GeometryType(geom), count(*) FROM parcels GROUP BY 1;
SELECT count(*) FROM parcels WHERE NOT ST_IsValid(geom);
-- extent looks right for the region
SELECT ST_Extent(geom) FROM parcels;
Compare the row count against ogrinfo -al -so parcels.shp (the "Feature Count"). Repair invalid geometries with UPDATE parcels SET geom = ST_MakeValid(geom) WHERE NOT ST_IsValid(geom);. Confirm the spatial index exists (\d parcels in psql) and, after a large load, run ANALYZE parcels; so the planner has fresh statistics.
Common pitfalls and why they happen
- Mixed single/multi geometry abort. Shapefiles allow both in one file; PostGIS columns are typed.
-nlt PROMOTE_TO_MULTIfixes it. - SRID 0 after load. The
.prjwas missing, so the CRS was never set. Assign the correct SRID afterwards — but only after verifying the coordinates match it. - Using
ST_SetSRIDwhen transformation was needed. Relabels without moving, leaving data in the wrong place. UseST_Transformwhen coordinates must change. - Truncated field names. DBF column names are capped at 10 characters; long attribute names arrive truncated, which can collide. Rename in PostGIS or remap with an OGR SQL clause.
- Encoding mismatch. Accented text becomes mojibake when the source codepage is not declared.
- No index, slow queries. Forgetting
-I/SPATIAL_INDEX=GISTmakes every spatial query a sequential scan.
Bathyl perspective
We load shapefiles through scripted ogr2ogr calls so the SRID, encoding, geometry promotion, and index are explicit and re-runnable, and every load ends with the validation queries above written to a log. A pipeline that asserts row count, SRID, and geometry validity catches the silent failures long before the data reaches a map or an analysis.
Related reading
- Export PostGIS Data to GeoJSON
- SQL for Spatial Quality Control
- Shapefile vs GeoPackage vs GeoJSON
- Spatial data products