<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pg_dump · XB Field Notes</title><link>https://www.xbcatm.com/tags/pg_dump/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/pg_dump/index.xml" rel="self" type="application/rss+xml"/><item><title>pg_dump captures a consistent snapshot</title><link>https://www.xbcatm.com/posts/postgres-pg_dump-consistent-backup/</link><pubDate>Fri, 14 Jan 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/postgres-pg_dump-consistent-backup/</guid><description>pg_dump runs a transaction that takes a stable snapshot, so the dump never sees a half-committed set of changes.</description><content:encoded>&lt;p&gt;&lt;code&gt;pg_dump&lt;/code&gt; exports one PostgreSQL database using a consistent view of transactional data, even while other sessions commit changes. This prevents the ordinary table-by-table copy problem in which related tables reflect different committed states. The snapshot is only part of the backup contract: the archive must also be complete for its intended scope, readable by compatible tools, and successfully restored into a usable database.&lt;/p&gt;
&lt;h2 id="know-what-the-snapshot-covers"&gt;Know what the snapshot covers&lt;/h2&gt;
&lt;p&gt;Rows visible to the dump&amp;rsquo;s snapshot remain its view throughout the export. Later commits do not gradually enter the dump just because one table is read after another. Parallel dumps synchronize workers&amp;rsquo; snapshots rather than independently observing each table. They require directory format; custom format supports parallel restoration, but not parallel dumping.&lt;/p&gt;
&lt;p&gt;A database snapshot does not make every business workflow atomic. If an application commits a batch header and its detail rows in separate transactions, a consistent snapshot can legitimately fall between those commits. Sequence state is also not ordinary MVCC row data, and external files or services are outside the database snapshot. Define the recovery invariant around actual transaction boundaries and all required data stores.&lt;/p&gt;
&lt;h2 id="account-for-load-and-dependencies"&gt;Account for load and dependencies&lt;/h2&gt;
&lt;p&gt;Dumping normally allows ordinary readers and writers to continue, but it still takes ACCESS SHARE locks and consumes CPU, I/O, connections, and snapshot lifetime. Conflicting schema changes can wait or make the dump fail. Long snapshots can prevent cleanup of old row versions. Parallel workers increase load; they do not make backup overhead disappear.&lt;/p&gt;
&lt;p&gt;A standby can offload some reads, but recovery conflicts and lag need consideration, and unlogged table contents are excluded when dumping from a standby. One pg_dump does not include cluster-wide roles and tablespaces. Extensions, ownership, permissions, collation behavior, and target configuration must be included in the restore plan through their appropriate mechanisms. Splitting an export into unrelated table dumps can lose the common snapshot and dependency closure.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;An archive listing can look healthy while restoration fails on a missing role or extension. Restore into an empty, isolated target with explicit error handling, then validate application invariants and representative queries. &lt;code&gt;pg_restore&lt;/code&gt; normally continues after SQL errors and reports an error count; checking only the last printed query is inadequate. A single-transaction restore can provide atomicity for a small rehearsal, but cannot be combined with parallel jobs.&lt;/p&gt;
&lt;p&gt;Tool versions are another boundary. A pg_dump client refuses a server newer than its own major version. Output is intended for the same or newer target versions; loading it into an older major version is not guaranteed. Verify the actual client, source, target, extensions, and restore options rather than relying on the generic claim that “Postgres backups are portable.”&lt;/p&gt;
&lt;p&gt;Logical exports can meet an explicitly measured recovery objective, but they do not provide arbitrary point-in-time replay between dumps. Physical base backups plus a complete retained WAL chain serve that requirement. The acceptable interval follows the required data-loss window and restore time, not a universal rule that anything shorter than a day requires one specific schedule.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save the Bash block as &lt;code&gt;dump.sh&lt;/code&gt;. It creates its own PostgreSQL cluster and removes it on exit. Run it with PostgreSQL 18 tools as a non-root user, or use this isolated container with no published port or host-data mount:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm --network none --user postgres --entrypoint bash -i &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; postgres:18-alpine &amp;lt; dump.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;dump_lab&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /tmp/pgdump-lab.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data/postmaster.pid&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pg_ctl -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; -m fast -w stop &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rm -rf -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; cleanup EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initdb -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; --no-locale --encoding&lt;span class="o"&gt;=&lt;/span&gt;UTF8 --auth&lt;span class="o"&gt;=&lt;/span&gt;trust &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_ctl -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; -l &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/server.log&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="s2"&gt;&amp;#34;-k &lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt; -p 55432 -c listen_addresses=&amp;#39;&amp;#39;&amp;#34;&lt;/span&gt; -w start &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;PGPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;55432&lt;/span&gt; &lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;source_db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DUMP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/source.dump&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;DUMP_OK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/dump-ok&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;createdb source_db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;createdb restored_db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -X -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CREATE TABLE customers (id integer PRIMARY KEY, name text);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CREATE TABLE orders (id integer PRIMARY KEY, customer_id integer REFERENCES customers(id));
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;INSERT INTO customers VALUES (1,&amp;#39;before&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;INSERT INTO orders VALUES (10,1);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SELECT pg_export_snapshot() AS snapshot_id \gset
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;\setenv DUMP_SNAPSHOT :snapshot_id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;\! psql -X -v ON_ERROR_STOP=1 -c &amp;#34;BEGIN; INSERT INTO customers VALUES (2,&amp;#39;after&amp;#39;); INSERT INTO orders VALUES (20,2); COMMIT;&amp;#34; &amp;amp;&amp;amp; pg_dump --format=custom --snapshot=&amp;#34;$DUMP_SNAPSHOT&amp;#34; --file=&amp;#34;$DUMP_FILE&amp;#34; &amp;amp;&amp;amp; touch &amp;#34;$DUMP_OK&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;COMMIT;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$DUMP_OK&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_restore --list &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$DUMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/archive-list&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_restore --exit-on-error --single-transaction --dbname&lt;span class="o"&gt;=&lt;/span&gt;restored_db &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$DUMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SELECT (SELECT count(*) FROM customers), (SELECT count(*) FROM orders)&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;psql -X -Atc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$counts&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;2|2&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;psql -X -d restored_db -Atc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$counts&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1|1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;psql -X -d restored_db -Atc &lt;span class="s1"&gt;&amp;#39;SELECT count(*) FROM orders o LEFT JOIN customers c ON c.id=o.customer_id WHERE c.id IS NULL&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -X -d restored_db -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; -c &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;SELECT c.name,o.id FROM customers c JOIN orders o ON o.customer_id=c.id&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_dump --version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;PASS: source=2|2 restored=1|1 orphan_orders=0\n&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first session exports a snapshot containing one customer and one order. While that transaction remains open, a second session commits another related pair. The dump explicitly imports the earlier snapshot; restoration therefore produces &lt;code&gt;1|1&lt;/code&gt;, while the live source reports &lt;code&gt;2|2&lt;/code&gt;. The join returns customer “before” with order ten, and an orphan check returns zero.&lt;/p&gt;
&lt;p&gt;On PostgreSQL 18.6, the complete restore and all assertions pass. The success marker ensures a failed nested shell dump cannot masquerade as success merely because psql continued. This demonstrates snapshot selection and restoration for the fixture, not a measured recovery time for the real application. Keep actual restore rehearsals, artifact checksums, retention, and dependency records with the production backup procedure.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/app-pgdump.html"&gt;PostgreSQL pg_dump&lt;/a&gt;, snapshots, formats, scope and version compatibility, checked 2026-09-10.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/app-pgrestore.html"&gt;PostgreSQL pg_restore&lt;/a&gt;, restore error handling and transaction options, checked 2026-09-10.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>