PostgreSQLIntermediate 13 min Lesson 23 of 40

Backup Fundamentals

The difference between logical backups with pg_dump and physical base backups, and when each approach is the right one.

PostgreSQL · Lesson 23 of 40
0/40 done(0%)

What is it? #

There are two fundamentally different ways to back up PostgreSQL, and choosing the wrong one wastes effort.

A logical backup (pg_dump) exports the contents: the SQL statements and data needed to rebuild your database. It is portable across versions and platforms, and you can restore a single table from it.

A physical backup (pg_basebackup) copies the files: the actual data directory, byte for byte. It is faster for large databases, restores as a complete cluster, and — critically — is the foundation for point-in-time recovery.

The decision comes down to size and recovery goals. Small database, want portability and selective restore: logical. Large database, want fast recovery to any moment: physical plus WAL archiving.

And one rule outranks both: a backup you have never restored is not a backup. It is a file you hope is a backup.

Think of it like this #

Two ways to back up a house.

A logical backup is a detailed inventory and instructions: every item, its description, how to rebuild the shelving. Rebuilding takes time, but you can do it in a different house, in a different city, and you can retrieve just the contents of one cupboard.

A physical backup is a photograph of every room, precise to the millimetre. Restoring is fast and exact — but it only works in an identical house, and you cannot extract a single cupboard from it.

Neither is useful if you have never tried rebuilding from them. That is the part people skip.

Simple example #

A 2 GB application database that must be restorable onto a developer's laptop, and occasionally needs one table recovered after a bad deployment. Logical backups fit perfectly.

A 900 GB production database with a requirement to recover to any point within the last week, losing at most a few minutes of data. That needs physical backups plus WAL archiving.

Code #

BASH
# ---------- LOGICAL BACKUP: pg_dump ----------

# Plain SQL format — human readable, restored with psql
pg_dump -U postgres -d shop -F p -f shop.sql

# Custom format — COMPRESSED, and allows SELECTIVE restore. Prefer this.
pg_dump -U postgres -d shop -F c -f shop.dump
#   -F c  custom format
#   -F p  plain SQL
#   -F d  directory format (allows PARALLEL dump and restore)
#   -F t  tar

# Directory format with 4 parallel jobs — much faster on big databases
pg_dump -U postgres -d shop -F d -j 4 -f shop_dumpdir/
BASH
# ---------- Useful pg_dump options ----------

pg_dump -U postgres -d shop -F c -f shop.dump \
    --no-owner \           # do not record ownership (eases restore elsewhere)
    --no-privileges \      # skip GRANT statements
    --verbose

# Just the schema, no data — useful for creating a matching empty database
pg_dump -U postgres -d shop --schema-only -f schema.sql

# Just the data
pg_dump -U postgres -d shop --data-only -F c -f data.dump

# Only certain tables
pg_dump -U postgres -d shop -t orders -t order_items -F c -f orders.dump

# Everything EXCEPT a huge table you can regenerate
pg_dump -U postgres -d shop --exclude-table-data=audit_log -F c -f shop.dump
BASH
# ---------- pg_dumpall: the whole server ----------

# pg_dump backs up ONE database and does NOT include roles or passwords.
# pg_dumpall covers the cluster-wide objects:

pg_dumpall -U postgres --globals-only -f globals.sql
#   roles, passwords, tablespaces  <- EASILY FORGOTTEN
#   Without these, a restore has no users to own or access anything.

pg_dumpall -U postgres -f everything.sql     # all databases + globals (plain SQL only)

# A common, sensible combination:
pg_dumpall --globals-only -f globals.sql          # small, fast
pg_dump -F c -d shop -f shop.dump                 # compressed, selective restore
BASH
# ---------- PHYSICAL BACKUP: pg_basebackup ----------

pg_basebackup \
    -h localhost -U replicator \
    -D /backup/base_$(date +%F) \   # destination directory
    -Ft -z \                        # tar format, gzip compressed
    -Xs \                           # stream WAL during the backup
    -P \                            # show progress
    -c fast                          # force an immediate checkpoint

#   -Xs is important: it streams the WAL generated DURING the backup,
#       so the result is self-consistent and restorable on its own.

# Requires a role with REPLICATION permission:
#   CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '...';
# and a pg_hba.conf line allowing "replication" connections.
TEXT
---------- Choosing between them ----------

                        LOGICAL (pg_dump)      PHYSICAL (pg_basebackup)
Backs up                SQL + data             the data directory files
Size                    smaller (compressed)   full cluster size
Speed on 500 GB         hours                  much faster
Restore granularity     one table, one schema  WHOLE CLUSTER ONLY
Cross-version restore   YES                    NO — same major version
Cross-platform          YES                    NO — same architecture
Restores users/roles    only via pg_dumpall    yes (whole cluster)
Point-in-time recovery  NO                     YES, with WAL archiving
Load on the server      reads every row        file copy, lighter
Good for                < ~100 GB, dev copies, large DBs, fast recovery,
                        migrations, selective  replication, PITR
                        restore
TEXT
---------- What backups protect against (and what they do not) ----------

A backup protects you from:
    accidental DELETE / DROP / bad migration
    data corruption
    a failed upgrade
    ransomware (IF the backups are offline or immutable)

A backup does NOT protect you from:
    a slow-burning bug that corrupted data weeks ago and was backed up
    faithfully every night since  <- this is why RETENTION matters

REPLICATION IS NOT A BACKUP.
A standby copies every change, including DROP TABLE, within seconds.
It protects against hardware failure, not against mistakes.
You need both.
TEXT
---------- RPO and RTO: the two numbers that decide everything ----------

RPO — Recovery Point Objective
      "How much data may we lose?"
      Nightly backups only        -> RPO up to 24 hours
      Nightly + WAL archiving     -> RPO of minutes

RTO — Recovery Time Objective
      "How long may recovery take?"
      500 GB logical restore      -> many hours
      500 GB physical restore     -> much faster

Decide these numbers with whoever owns the business risk, THEN design the
backup strategy. Working the other way round produces a strategy nobody
has agreed to and that does not meet anyone's actual requirements.
BASH
# ---------- Verifying a backup is real ----------

# A file that exists is not proof. Check it can be READ:
pg_restore --list shop.dump | head -20
# Lists the contents. If this fails, the dump is corrupt.

# Check it is not suspiciously small:
ls -lh shop.dump

# THE ONLY REAL TEST: restore it somewhere and query it.
createdb -U postgres restore_test
pg_restore -U postgres -d restore_test shop.dump
psql -U postgres -d restore_test -c "SELECT count(*) FROM orders;"
dropdb -U postgres restore_test

# Do this on a SCHEDULE, not just when you set the backups up.

How it works #

pg_dump connects as an ordinary client and reads the database inside a single transaction using a consistent snapshot. That means the dump reflects one exact moment even while the database is being written to, and it does not block other users. It does read every row, so it puts real load on the server and takes time proportional to the data.

Because the output describes contents rather than files, a logical dump is portable: dumped from PostgreSQL 14, restored into 16, on a different operating system and CPU architecture. That portability is what makes it the right tool for upgrades and for moving data around.

The custom format (-F c) is compressed and indexed, which is what allows pg_restore to extract a single table from it. Plain SQL (-F p) can only be replayed from beginning to end. Directory format (-F d) adds parallelism, which matters a great deal on large databases.

The most commonly missed detail: pg_dump does not back up roles or passwords. Those live at cluster level, not inside a database. Restoring a dump onto a fresh server without pg_dumpall --globals-only leaves you with tables nobody owns and no users who can connect — usually discovered at the worst possible moment.

pg_basebackup copies the data directory itself. It cannot be selective, because the files only make sense as a complete set. With -Xs it also streams the WAL generated during the copy, which is what makes the result internally consistent: the file copy takes time, the database changes while it runs, and those changes are captured in the streamed WAL.

That property is what makes a physical backup the foundation for point-in-time recovery. Restore the base backup, then replay archived WAL forward to any chosen moment. A logical dump cannot do this — it is a snapshot of one instant with no mechanism to move forward from it.

RPO and RTO are the numbers that should drive the design. If losing a day of data is acceptable, nightly logical dumps may be entirely sufficient. If only minutes may be lost, WAL archiving is required regardless of database size. Agreeing these explicitly prevents both under-engineering and expensive over-engineering.

Real-world use #

The failure that actually happens is not a missing backup — it is a backup that turns out to be unrestorable at the moment it is needed. Empty files from a cron job whose credentials expired. Dumps of the wrong database. A perfect dump of the data with no roles to own it. All of these are common, and all are caught by periodically restoring and querying.

Schedule restore tests. Monthly is reasonable for most systems. Restore into a scratch database, run a few queries that check real row counts, and record that it worked. This is the single highest-value practice in this entire track.

Keep backups off the database server. A backup on the same disk does not survive the disk failing, and a backup on the same machine does not survive that machine being compromised. Copy them to object storage or another host, and for ransomware protection prefer storage with immutability or versioning enabled.

Retention needs thought beyond "keep seven days". A bug that corrupts data slowly may not be noticed for weeks, by which time every retained backup contains the corruption. A common pattern is daily backups for a week, weekly for a month, monthly for a year — enough history to reach back past a slow-burning problem.

Be explicit that replication is not a backup. A standby applies DROP TABLE as faithfully as it applies an INSERT, within seconds. It protects against a server dying, not against a mistake or a bug. Systems need both, and conflating them is a genuinely dangerous assumption.

Finally, write the restore procedure down, as a checklist, somewhere reachable when the database is down. Recovery happens under pressure, often by whoever is available rather than whoever designed it.

Common mistakes #

  • Never testing a restore, so the first attempt happens during a real incident.
  • Backing up databases with pg_dump but never pg_dumpall --globals-only, losing all roles and passwords.
  • Storing backups on the same server or disk as the database they protect.
  • Treating a replica as a backup — it copies mistakes like DROP TABLE within seconds.
  • Keeping only a few days of backups, so a slow-burning corruption is present in every one of them.

Practice #

Take a custom-format pg_dump of a test database and a separate pg_dumpall --globals-only. Inspect the dump with pg_restore --list to see its contents. Then restore it into a brand-new database, query a table to confirm the row count matches, and drop the test database. Write down the exact commands as a short restore checklist — that document is more valuable than the backup itself.

Quick quiz

  1. 1. What does pg_dump NOT back up?

  2. 2. Which backup type supports restoring a single table?

  3. 3. Why is a replica not a backup?

  4. 4. What does RPO measure?

  5. 5. Which backup approach is required for point-in-time recovery?

Summary

  • Logical backups (pg_dump) are portable and allow selective restore; physical backups copy files.
  • pg_dump does not include roles or passwords — pair it with pg_dumpall --globals-only.
  • Only physical backups plus WAL archiving support point-in-time recovery.
  • Replication is not a backup: it copies DROP TABLE as faithfully as anything else.
  • A backup that has never been restored is not a backup — test restores on a schedule.