Topic 64

Logical Backups — pg_dump and pg_restore

Logical Backup

pg_dump writes one database out as SQL statements or as an archive file, and what lands in that file is the database exactly as it existed the moment the dump started. It runs while the application keeps working, blocking neither readers nor writers, and the result is portable in a way nothing else in this chapter is: across major versions, across architectures, onto a laptop.

It is also the reason the answer at 14:12 was "restore last night and lose the day". Nadia's nightly dumps were doing precisely what they are designed to do, which is capture one instant per run. The strategy built on top of that was the failure, and the properties that disqualify a dump for Cartwheel are structural rather than fixable. This topic covers what a dump is genuinely good for, which is more than the incident makes it sound.

What a Dump Produces

The output format matters more than it looks, and there are four. Plain SQL (-Fp) is the default and is a text file of statements that psql replays. The custom archive (-Fc) is compressed and allows selective restore from a single file. The directory format (-Fd) writes one file per table plus a table of contents, and it is the only format that supports a parallel dump with -j, because it is the only one where several processes can write at once. Tar (-Ft) exists and is rarely the right answer. Compression is chosen separately with -Z, which accepts gzip, lz4, zstd or none.

Four output formats, and what each one costs at restore time
Plain-Fp, the default
A text file of SQL statements. pg_restore will not read it — it goes back through psql, which needs ON_ERROR_STOP=on to stop at the first failure instead of finishing with a success.
Custom-Fc
Compressed, and it allows a selective restore from a single file. Being a single file is also why it cannot be dumped in parallel.
Directory-Fd
One file per table plus a table of contents — the only format that supports a parallel dump with -j.
Tar-Ft
Read by pg_restore like the other two archive formats, and rarely the right answer.

The consistency guarantee is worth stating precisely, because it is stronger than people expect and it has a price attached. The dump is internally consistent, a snapshot of the database as of when pg_dump began, and it does not block other operations while it runs. The mechanism is a single transaction holding a single snapshot for the entire duration, which means that while a four-hour dump runs, the oldest snapshot the cluster must preserve is four hours old. Dead row versions anywhere in the cluster stay uncollectable for that whole window, on every table, including the ones the dump has already finished reading.

What a Dump Does Not Contain

pg_dump dumps one database, and roles, tablespaces and cluster-wide grants do not live inside any database. They come from pg_dumpall, whose --globals-only flag writes exactly those objects and no table data at all. Skip it and the restore produces a cluster where nothing has an owner and nobody can log in: cartwheel_app, cartwheel_analytics, cartwheel_migrator and cartwheel_admin do not exist, so every GRANT in the dump fails and the application cannot connect to the database that was just successfully restored.

The two files that have to be produced together
pg_dump -Fd -j 4 -Z zstd \
        -f /backup/cartwheel-2026-08-13 cartwheel

pg_dumpall --globals-only \
        -f /backup/globals-2026-08-13.sql     -- roles and tablespaces

# restoring the pair, globals first:
psql -X --set ON_ERROR_STOP=on -f /backup/globals-2026-08-13.sql postgres
pg_restore -j 4 -d cartwheel /backup/cartwheel-2026-08-13

The first command dumps four tables at a time into a directory; the second writes the roles and tablespaces as plain SQL, which is the only thing pg_dumpall produces. Order matters on the way back: the globals must exist before anything tries to grant privileges to them, and restoring them needs superuser access, since recreating roles and tablespaces is a superuser operation by definition. Store the two files together with the same date in the name; a globals file from three months ago restores a permission model from three months ago, and every grant in it will apply cleanly.

Restoring

pg_restore reads the non-plain formats: custom, directory and tar. A plain dump is not its business at all; that one goes through psql, which by default keeps going after an SQL error and finishes reporting success, so --set ON_ERROR_STOP=on is the difference between a restore and a story about a restore. pg_restore's default is the same shape: without -e it continues past errors and prints a count at the end, on a terminal that has just scrolled several thousand lines.

Most of the value on the way back sits in three flags. -j runs the expensive steps concurrently: loading the data, building the indexes, validating the constraints. It works with the custom and directory formats, though not together with --single-transaction. -1 wraps the whole restore in one transaction so a failure leaves nothing behind, and it implies exit-on-error. --section splits the work into pre-data, data and post-data, which is how you load rows into a schema that already exists. Behind all three sits the number that decides whether this is a backup strategy at all: restoring is dramatically slower than dumping, because the dump copies rows while the restore rebuilds every index and revalidates every constraint from scratch.

Version Portability

This is the capability physical backups do not have, and the reason a dump belongs in the toolbox permanently. The output of pg_dump can be loaded into servers newer than the pg_dump that produced it, and pg_dump can read servers older than itself, back to 9.2. What it will not do is dump from a server newer than its own major version; it refuses outright rather than risk producing an invalid dump. The rule that falls out is one line long and it is the one people get backwards: use the newer side's binaries. Pointing a laptop's 17 pg_dump at Cartwheel's 18 cluster fails immediately; pointing 18's pg_dump at a 17 cluster is exactly the supported path, and it is how the upgrade in Chapter 14 moves data when it has to.

Selective Work

A dump can be narrowed: -n analytics for one schema, -t orders for one table, --schema-only or --data-only to take half of it. This is the everyday use that keeps pg_dump in daily rotation even on a cluster whose real backups are physical. Staging gets refreshed with the schema plus the small tables (products, product_categories, order_statuses, couriers) and none of the 40 million orders. An analyst gets one table on a laptop instead of a read-only role on the replica. And when 90,000 rows have to come back into a live production table, the mechanism that carries them is a single-table dump taken from a scratch cluster, which is exactly where topic 66 ends up.

What a dump answers, and where the physical pair takes over
Move a database across a major versionpg_dump
Refresh staging with the schema and the small tablespg_dump
Put one table on an analyst's laptoppg_dump -t
Recover to 14:11 rather than to the last dumpa physical backup
Restore several hundred gigabytes inside a thirty-minute objectivea physical backup

Where It Stops as a Backup Strategy

What disqualifies it as Cartwheel's only backup is three properties, all of them structural. A dump captures exactly one moment, the instant it began, so no accident can ever be answered with anything other than "restore the most recent dump", regardless of when the accident happened. Restore time is dominated by index rebuilds rather than by data volume, which on a several-hundred-gigabyte database is hours against a recovery objective measured in tens of minutes. And every run holds a snapshot open on the source for its full duration.

The second and third are mitigable. Take the dump from pg-replica-a and the primary stops paying for the held snapshot, since a dump is a read like any other. Use the directory format with -j on both ends and the wall-clock numbers improve considerably. The first one is not mitigable by any flag, because the information needed to reach 14:11 was never written into the file. Topic 65 is where that information comes from. Cartwheel keeps the nightly dump anyway, for the portability, the staging refreshes and the single-table extractions that a physical backup cannot do at all.

Logical dump vs physical backup

A logical dump is portable across major versions and architectures, selective down to a single table, readable as text in plain format, and slow to restore because every index and constraint is rebuilt. It captures one instant and offers no way to recover to any other.

A physical backup — a byte-level copy of the data directory plus the WAL written since, fast to restore, recoverable to any moment inside the archived window, and tied to the same major version and platform. It cannot extract one table and cannot cross a version boundary.

Keeping both — a database small enough to restore inside its recovery objective can live on dumps alone. Anything with a recovery-time promise needs the physical pair, and most teams keep dumps alongside it for reasons that have nothing to do with disaster.

Common Mistakes
  • Dumping databases nightly and never running pg_dumpall --globals-only — the restore comes up with no roles and no grants, and the discovery happens mid-incident.
  • Treating a nightly dump as the backup strategy for a database whose restore takes six hours, against a recovery objective of thirty minutes that somebody has already put in writing.
  • Running a multi-hour pg_dump against the primary during business hours — the snapshot it holds pins the cluster's vacuum horizon for the entire run.
  • Using the older major's pg_dump when moving between versions, which fails outright against a newer server rather than producing something questionable.
  • Restoring a plain dump with psql and no ON_ERROR_STOP — it fails halfway, keeps going, and exits reporting success.
  • Choosing custom format for a large dump and then wondering why it took all night, when only the directory format can dump in parallel.
Best Practices
  • Pair every database dump with a pg_dumpall --globals-only file taken at the same time, stored beside it and named with the same date.
  • Use the directory format with -j for anything large, and restore with pg_restore -j on hardware that has the cores to use.
  • Run scheduled logical dumps against pg-replica-a so the primary never pays for the snapshot they hold.
  • Restore plain dumps with ON_ERROR_STOP=on, and use pg_restore -1 when a partial restore would be worse than none.
  • Keep pg_dump for migrations, staging refreshes and single-table extraction, and build the actual backup strategy on physical backups plus archived WAL.
  • Time a full restore once and write the number down, since it is the number your recovery objective is really made of.
Comparable toolsmysqldump and mydumper the same tradeoffs, globals problem includedOracle Data Pump export and importSQL Server BACPAC and scripted exportspgcopydb for large logical migrations between clusters

Knowledge Check

A restore from last night's dumps comes up but the application cannot connect. What was most likely missed?

  • The globals file, so no roles exist for the dump's grants to be applied to
  • The schema section, so the tables were restored without their definitions
  • The post-data section, so the indexes and constraints are all missing
  • The pg_hba.conf entries, which pg_dump stores alongside the database

Which pg_dump format lets several processes dump different tables at the same time?

  • Directory format, since each worker writes its own file
  • Custom format, since the archive index tracks each worker
  • Plain format, since SQL statements can be interleaved freely
  • Tar format, since members can be appended independently

You are moving a database from a 17 cluster to an 18 cluster. Whose pg_dump should run?

  • The 18 binaries, which can read the older 17 server safely
  • The 17 binaries, which match the server being read
  • Either one, since the dump format is version independent
  • Neither, since dumps cannot cross a major version at all

Why can a nightly pg_dump never answer the 14:12 accident with "restore to 14:11"?

  • The file contains one instant, and 14:11 is not the instant it captured
  • The restore takes longer than the outage that it is meant to repair
  • A dump cannot restore one table, and only orders needs recovering
  • The dump is inconsistent, so mid-afternoon states cannot be trusted

What does a four-hour pg_dump cost the cluster it runs against, beyond I/O?

  • A four-hour-old snapshot that stops dead rows being removed anywhere
  • An exclusive lock on each table for as long as that table is dumped
  • A forced checkpoint at the start of every table it copies out
  • Loss of the buffer cache, since dumps bypass shared_buffers entirely

You got correct