Load GeoTIFF into PostGIS with raster2pgsql
raster2pgsql turns a GDAL-readable raster into SQL, and the wrong flag can quietly drop a table you meant to keep. This walks through generating that SQL, reviewing it, loading it into a PostGIS raster table, and checking the result actually matches the source: about 20 minutes for a small raster.
The route
Jump straight to the step you need, or tick off Done means at the end.
- You need: the
postgispackage, a readable input file, thepsqlclient, and a database with the PostGIS extension already enabled. - Permissions: generating SQL is an ordinary user command. Loading it needs a PostgreSQL role that can create or write the target table. Nothing here needs a root shell.
The examples use the installed raster2pgsql from PostGIS 3.4.2, package version 3.4.2+dfsg-1ubuntu3, with GDAL 3.8 support.
1. Check the loader and the input format
The version line hides in the help output rather than behind a conventional --version flag:
$ command -v raster2pgsql
/usr/bin/raster2pgsql
$ raster2pgsql -? 2>&1 | head -4
RELEASE: 3.4.2 GDAL_VERSION=38 (c19ce56)
USAGE: raster2pgsql [options] raster [raster ...] [schema.table]
Then confirm the source file is readable and the format is actually supported, before you generate a single line of SQL:
$ test -r /path/to/input.tif && echo readable
readable
$ raster2pgsql -G | grep -E 'GeoTIFF|Virtual Raster'
Virtual Raster
GeoTIFF
That -G list belongs to this executable and its GDAL build. A format supported on another machine is not automatically supported here. If your file format is missing, stop and get a loader built with the right GDAL driver.
2. Choose a target and a tile size
Use a schema and table name with no existing data for the first load. The command takes one or more raster paths, shell wildcards, and an optional schema.table destination.
- Pick a tile size deliberately. A raster table is stored as rows of tiles.
256x256is a practical starting point for many datasets, not a universal optimum. - Set the SRID when source metadata is missing or you have confirmed the real coordinate reference system. The
-svalue is not a reprojection, it only labels the raster metadata. Leave it out or set it to zero and the loader reads the raster's own metadata instead.
Checkpoint
Write down these values before running a load.
- Input:
/path/to/input.tif - Database:
gisdb - Target:
public.elevation - Known SRID:
27700, only if that is genuinely the source CRS
3. Generate SQL you can read before it touches the database
The default -c mode creates and populates a new table, and -I adds a GiST spatial index. The command writes SQL to standard output, so redirect it to a file rather than piping straight into a production database on the first run:
$ raster2pgsql -c -s 27700 -t 256x256 -I -C \
/path/to/input.tif public.elevation > /tmp/elevation.sql
$ test -s /tmp/elevation.sql && echo 'SQL generated'
SQL generated
$ sed -n '1,24p' /tmp/elevation.sql
-Cadds the standard raster constraints after loading. They can fail when your input rasters don't share the properties those constraints require.-Ialso triggers anANALYZEfor the created index.- Read the file before running it. A generated SQL file is data that can create or alter database objects, so never run one from an untrusted source.
4. Load the new table
Run the reviewed file with the connection details for your environment. This example uses a local database and PostgreSQL's normal password and connection setup:
$ psql --dbname=gisdb --file=/tmp/elevation.sql
CREATE TABLE
INSERT 0 1
CREATE INDEX
ANALYZE
The exact number of INSERT messages depends on the raster and tile size, and PostGIS may add its own notices. Treat a non-zero psql exit status as a failed load, and inspect the first error rather than assuming later messages repaired it.
Warning
Do not substitute -d for -c casually. The -d mode drops the named table and recreates it: destructive and irreversible unless you have a backup or can rebuild the data. Use -c for a new table, -a only when the existing table has exactly the same schema, and -p when you only want table-creation SQL.
5. Verify the table and the raster metadata
Check the table exists, has rows, and reports the raster properties you expected:
$ psql --dbname=gisdb --command="\d+ public.elevation"
$ psql --dbname=gisdb --command="
SELECT count(*) AS tiles,
ST_Width(rast) AS tile_width,
ST_Height(rast) AS tile_height,
ST_SRID(rast) AS srid
FROM public.elevation
GROUP BY ST_Width(rast), ST_Height(rast), ST_SRID(rast)
ORDER BY tiles DESC;"
tiles | tile_width | tile_height | srid
-------+------------+-------------+------
...| 256 | 256 | 27700
The final tile on an edge can be smaller than 256x256. Add -P if every tile must have the same dimensions; it pads the right-most and bottom-most tiles, which changes the stored edge data, so only use it when your downstream processing benefits from regular blocks.
Compare the SRID against the source dataset's documented CRS. A plausible-looking number does not prove the raster is correctly located. If reprojection is needed, do it as a separate, reviewed GDAL operation first: raster2pgsql does not reproject pixels just because -s was supplied.
6. Handle bands, filenames and overviews
By default every band is extracted.
- Select particular bands with
-b, one-based, including ranges such as1-3or a comma-separated list. - Store the source filename with
-F, or use-n source_file, which implies-Fas well.
$ raster2pgsql -c -s 27700 -b 1 -t auto -n source_file \
/path/to/tiles/*.tif public.dem > /tmp/dem.sql
$ psql --dbname=gisdb --file=/tmp/dem.sql
The auto tile size is calculated from the first raster and applied to all of them, which makes the order and contents of a wildcard meaningful: use explicit file paths, or review the expansion when reproducibility matters. An overview factor such as -l 2,4 creates overview tables named with the documented o_<factor>_<table> pattern. Overviews live in the database, not as out-of-database file references.
7. Recover from a failed or unwanted load
If SQL generation failed, the input file was never touched. If psql failed partway through a generated script, inspect the database before retrying, since some statements may already have run. For a disposable table, remove it explicitly after checking the name:
$ psql --dbname=gisdb --command="DROP TABLE IF EXISTS public.elevation CASCADE;"
CASCADE can remove dependent objects, so never run that against a shared table without confirming the dependency impact first. For a production load, restore from backup or use a new table name, validate it, then arrange a reviewed cutover. Keep the original raster files until the SQL load and metadata checks pass.
Done means
- Version checked: the installed PostGIS and GDAL versions, and the supported input driver.
- SRID confirmed: taken from verified source metadata, not guessed from a filename.
- SQL reviewed: generated and read before it was sent to PostgreSQL.
- Right mode used: create, append or prepare as intended, with destructive
-davoided unless explicitly authorised. - Table verified: expected tile dimensions, SRID and row count confirmed.
- Recovery kept: the source raster and generated SQL remain available for audit.