pg_dumpとは、PostgreSQLのデータベース1つをSQLまたはアーカイブ形式に書き出す論理バックアップのコマンドで、pg_restoreはそのアーカイブを別のデータベースに戻すコマンドです。 書き出す形式はplain(SQLテキスト)・custom・directory・tarの4つがあり、「どの形式で取ったか」で戻し方(psqlかpg_restoreか)と並列リストアの可否が決まります。ここを押さえるだけで、バックアップ運用の大半は迷わなくなります。

この記事では、PostgreSQL 16.13 に約145万行のサンプルDB(178MB)を作り、4形式のダンプ時間とファイルサイズ、gzip/lz4/zstdの圧縮比較、別DBへのリストアと件数照合、permission denied や role does not exist などの実際のエラー文、cronで回す日次バックアップ+世代管理スクリプトまで、すべて実行した結果を載せています。実行時間は2vCPUのコンテナでの参考値で、台数や負荷で変わります。

pg_dump / pg_restore の早見表

まず「形式ごとに何ができるか」の対応表です。迷ったら custom 形式(-Fc)に zstd 圧縮、テーブル数が多く時間が問題なら directory 形式(-Fd -j)を選びます。

形式 オプション 戻し方 並列ダンプ -j 並列リストア -j 圧縮 一部だけ戻す 向いている場面
plain(SQL) -Fp(既定) psql -f 不可 不可 任意(--compress) エディタで削る 中身を読みたい・別DB製品へ移す前段
custom -Fc pg_restore 不可 可 既定で gzip。lz4/zstd 可 -l/-L、-t、-n で可 日常のバックアップの第一候補
directory -Fd pg_restore 可(唯一) 可 既定で gzip。lz4/zstd 可 可 大きいDB・時間短縮
tar -Ft pg_restore 不可 不可 不可 可(順序変更は不可) 他ツールと組み合わせて展開したい

よく使うコマンドの逆引きです(詳細と実行結果は本文)。

やりたいこと コマンド
DB全体をcustom形式・zstd圧縮で取る pg_dump -Fc --compress=zstd -d mydb -f mydb.dump
並列2でdirectory形式に取る pg_dump -Fd -j 2 -d mydb -f mydb_dir
空のDBへ戻す(並列2) createdb -T template0 newdb && pg_restore -j 2 -d newdb mydb.dump
既存DBを上書きして戻す pg_restore --clean --if-exists -d mydb mydb.dump
所有者・権限を持ち込まずに戻す pg_restore --no-owner --no-privileges -d newdb mydb.dump
スキーマだけ/データだけ pg_dump -s ... / pg_dump -a ...
特定テーブル/スキーマだけ pg_dump -t app.orders ... / pg_dump -n app ...
テーブルを除外(連番も一緒に) pg_dump -T 'app.access_logs*' ...
ロール・テーブルスペースを取る pg_dumpall --globals-only -f globals.sql
別DBへ直接コピー pg_dump -d src | psql -d dst
ダンプの目次を見る pg_restore -l mydb.dump
plainのSQLを全部か無しかで流す psql -1 -v ON_ERROR_STOP=1 -d newdb -f mydb.sql

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「pg_dump/pg_restoreの使い方|バックアップとリストアの実行例」

検証環境とサンプルDB

項目 内容
PostgreSQL 16.13(Ubuntu 24.04 パッケージ)、2vCPU・メモリ7GB のコンテナ
DB t59(178MB)。スキーマ app と archive
テーブル app.orders 100万行、app.access_logs 30万行、archive.orders_2024 10万行、app.members 5万行、app.items 2,000行
その他 ビュー1、関数1、拡張 pg_trgm(GINインデックス)、所有者が違うテーブル(t59_app)、権限を絞ったロール(t59_reader)

サンプルは注文・会員・商品という中立な構成です。所有者や権限、拡張をわざと混ぜてあるのは、リストア時に実際に出るエラーを再現するためです。

形式 × リストア方法 × 並列の対応表(実測つき)

同じDBを4形式+圧縮方式ちがいでダンプし、それぞれ空のDBへ戻して件数を確認しました。数値は同条件で2回実行した値の範囲です。

ダンプ:形式・圧縮ごとの時間とサイズ

形式・圧縮 コマンド ダンプ時間 サイズ
plain(無圧縮) pg_dump -Fp -d t59 -f t59_plain.sql 0.7秒 72,124KB
plain+gzip pg_dump -Fp --compress=gzip -d t59 -f t59_plain.sql.gz 1.6〜1.8秒 15,545KB
custom(既定=gzip) pg_dump -Fc -d t59 -f t59_custom.dump 1.8〜1.9秒 15,567KB
custom+lz4 pg_dump -Fc --compress=lz4 ... 0.7秒 27,225KB
custom+zstd pg_dump -Fc --compress=zstd ... 0.8〜0.9秒 9,119KB
custom+zstd:long pg_dump -Fc --compress=zstd:long ... 0.7〜0.8秒 8,891KB
custom(無圧縮) pg_dump -Fc --compress=none ... 0.9秒 79,218KB
directory(gzip、-j 1) pg_dump -Fd -d t59 -f t59_dir 1.6〜1.7秒 15,564KB
directory(gzip、-j 2) pg_dump -Fd -j 2 -d t59 -f t59_dir 1.4秒 15,564KB
directory(zstd、-j 2) pg_dump -Fd -j 2 --compress=zstd ... 0.9秒 9,132KB
tar pg_dump -Ft -d t59 -f t59_tar.tar 0.8秒 72,144KB

この環境では zstd が「gzip より速く、サイズは6割」という結果でした。lz4 はさらに速い代わりにサイズが大きく、ネットワーク越しにコピーするなら zstd、ローカルで一瞬で取りたいなら lz4、という使い分けになります。lz4/zstd は PostgreSQL 16 のリリースノートに「Add LZ4 and Zstandard compression to pg_dump」として追加された機能で、--compress=方式[:レベル] の書き方も16で統一されました(例:--compress=gzip:9、--compress=zstd:long)。

tar 形式は圧縮できません。指定すると次のように止まります。

$ pg_dump -Ft --compress=zstd -d t59 -f t59.tar
pg_dump: error: compression is not supported by tar archive format

リストア:形式・並列ごとの時間

空のDB(createdb -T template0)へ戻し、3回実行した中央値です。

ダンプ形式 リストアのコマンド 時間(中央値)
plain psql -X -q -v ON_ERROR_STOP=1 -d t59_restore -f t59_plain.sql 3.6秒
custom(gzip) pg_restore -d t59_restore t59_custom.dump 3.5秒
custom(gzip)-j 2 pg_restore -j 2 -d t59_restore t59_custom.dump 4.1秒(3.4〜5.8秒とばらつき大)
directory -j 1 pg_restore -d t59_restore t59_dir 3.8秒
directory -j 2 pg_restore -j 2 -d t59_restore t59_dir 3.5秒
custom(zstd)-j 2 pg_restore -j 2 -d t59_restore t59_custom_zstd.dump 2.8秒
tar pg_restore -d t59_restore t59_tar.tar 3.6秒

並列 -j の効果が小さいのは、100万行の orders 1テーブルが全体の大半を占めているからです。並列リストアは「テーブル単位のデータ投入」と「インデックス・制約の作成」を別ジョブに振り分けるので、テーブルやインデックスが多いDBほど効きます。1つの巨大テーブルは1ジョブでしか読み込めないため、その時間が下限になります。目安は -j に CPU コア数(2vCPU なら 2)です。

並列に関する制約も、実際のエラー文で確認しておきます。

$ pg_dump -Fc -j 2 -d t59 -f x.dump
pg_dump: error: parallel backup only supported by the directory format

$ pg_restore -1 -j 2 -d t59_verify t59_custom.dump
pg_restore: error: cannot specify both --single-transaction and multiple jobs

$ pg_restore -d t59_verify t59_plain.sql
pg_restore: error: input file appears to be a text format dump. Please use psql.

$ psql -d t59_verify -f t59_custom.dump
The input is a PostgreSQL custom-format dump.
Use the pg_restore command-line client to restore this dump to a database.

目的別コピペ集(pg_dump)

すべて PostgreSQL 16.13 で実行し、出力を確認したものです。接続先は -h ホスト -p ポート -U ユーザー を足すか、環境変数 PGHOST PGPORT PGUSER で指定します。

DB全体(推奨形)

# custom 形式・zstd 圧縮。日常バックアップの第一候補
pg_dump -Fc --compress=zstd -d t59 -f t59.dump

# テーブルが多いDBは directory 形式で並列に
pg_dump -Fd -j 2 --compress=zstd -d t59 -f t59_dir

directory 形式の出力先に同名ディレクトリがあると止まります(上書きしません)。

pg_dump: error: could not create directory "t59_dir": File exists

スキーマだけ・データだけ

pg_dump -s -d t59 -f t59_schema.sql   # --schema-only:定義だけ(9KB)
pg_dump -a -d t59 -f t59_data.sql     # --data-only:COPY文だけ(72MB)

-s のファイルには CREATE TABLE が5つ、-a のファイルには COPY ... FROM stdin; が5つ入っていました。定義とデータを分けて取ると、「定義だけ先に流して確認し、データは夜間に流す」といった運用ができます。 3段階に分ける --section=pre-data|data|post-data(post-data がインデックス・制約・トリガー)もあります。

特定のテーブル・スキーマだけ

pg_dump -t app.orders  -d t59 -f orders_only.sql    # テーブル1つ(-t は複数指定可)
pg_dump -n archive     -d t59 -f archive_only.sql   # スキーマ1つ
pg_dump -t 'app.order*' -d t59 -f orders_like.sql   # パターン(psql の \d と同じ書き方)

-t app.orders の出力には、テーブル本体・所有者・連番(シーケンス)・インデックス・制約が含まれます。

CREATE TABLE app.orders (
ALTER TABLE app.orders OWNER TO t59_app;
CREATE SEQUENCE app.orders_order_id_seq
ALTER SEQUENCE app.orders_order_id_seq OWNED BY app.orders.order_id;
COPY app.orders (order_id, member_id, item_id, quantity, note, ordered_at) FROM stdin;
CREATE INDEX orders_member_idx ON app.orders USING btree (member_id);
CREATE INDEX orders_note_trgm_idx ON app.orders USING gin (note public.gin_trgm_ops);

一方で、参照先のテーブル(members、items)や拡張 pg_trgm は含まれません。 公式ドキュメントにも「-t を指定したとき、依存する他のオブジェクトはダンプしない」とあり、空のDBにそのまま戻すと外部キーや GIN インデックスの作成で止まります。戻す先に依存オブジェクトがあるか、先に -s で全体の定義を流しておきます。

テーブルを除外する(連番の落とし穴つき)

pg_dump -T app.access_logs -d t59 -f no_logs.sql                # テーブルを除外
pg_dump --exclude-table-data=app.access_logs -d t59 -f f.sql   # 定義は残しデータだけ除外

-T app.access_logs で除外しても、bigserial が作った連番 app.access_logs_log_id_seq は別オブジェクトなので残ります(grep で5行ヒット)。連番ごと除外するには -T 'app.access_logs*' とワイルドカードにします(ヒット0行になることを確認済み)。ログ系の大きなテーブルを日次から外すときに効く書き方です。

上書きリストア用:--clean --if-exists

pg_dump --clean --if-exists -d t59 -f t59_clean.sql

plain 形式の先頭に DROP ... IF EXISTS が並びます。

DROP SEQUENCE IF EXISTS app.items_item_id_seq;
DROP TABLE IF EXISTS app.items;

--if-exists は --clean とセットでしか使えません(pg_dump: error: option --if-exists requires option -c/--clean)。custom/directory 形式では、これらは pg_restore 側で指定します。

所有者・権限を持ち込まない:--no-owner --no-privileges

pg_dump --no-owner --no-privileges -t app.orders -d t59 -f orders_portable.sql

指定なしでは ALTER TABLE app.orders OWNER TO t59_app; が出力され、戻す先に t59_app ロールがないとエラーになります。指定すると OWNER TO と GRANT が0行になり、戻した人がすべての所有者になるので、開発環境へのコピーに向きます。

ロールとテーブルスペース:pg_dumpall --globals-only

pg_dumpall --globals-only -f globals.sql

pg_dump はDB1つ分しか取らず、ロール(ユーザー)とテーブルスペースは含みません。 --globals-only で取った SQL にはパスワードのハッシュも入っています。

CREATE ROLE t59_app;
ALTER ROLE t59_app WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'SCRAM-SHA-256$4096:qFTv...';
CREATE ROLE t59_reader;

新しいサーバーへ移すときは「globals.sql を先に流す → 各DBを pg_restore」の順にすると、所有者エラーが出ません。

別DBへ直接コピー:pg_dump | psql

createdb -T template0 t59_copy
pg_dump -d t59 | psql -X -q -v ON_ERROR_STOP=1 -d t59_copy

ファイルを作らずにコピーします(4.3秒、100万行を確認)。別ホストなら pg_dump -h host1 -d src | psql -h host2 -d dst です。ファイルが残らないので「検証用DBを毎朝作り直す」用途に向きます。

INSERT 文で出す(他DB製品や小さな差分に)

pg_dump -a -t app.items --inserts -d t59
pg_dump -a -t app.items --column-inserts --on-conflict-do-nothing -d t59
pg_dump -a -t app.items --rows-per-insert=1000 -d t59
INSERT INTO app.items VALUES (1, 'item_1', 137);
INSERT INTO app.items (item_id, item_name, price) VALUES (1, 'item_1', 137) ON CONFLICT DO NOTHING;

既定の COPY より読み込みは遅くなりますが、--on-conflict-do-nothing を付けると既存行を飛ばして流せるので、マスタ系の小さなテーブルを追記するときに便利です。

実際に実行したログ:ダンプ → 別DBへリストア → 件数確認

日常運用の形(custom+zstd、並列2)をそのまま流したログです。

# 1) ダンプ
$ pg_dump -Fc --compress=zstd -d t59 -f t59_custom_zstd.dump   # 0.8秒、9,119KB

# 2) 目次を確認(壊れていればここで読めない)
$ pg_restore -l t59_custom_zstd.dump | head -11
;
; Archive created at 2026-10-01 07:06:39 JST
;     dbname: t59
;     TOC Entries: 50
;     Compression: zstd
;     Dump Version: 1.15-0
;     Format: CUSTOM
;     Integer: 4 bytes
;     Offset: 8 bytes
;     Dumped from database version: 16.13 (Ubuntu 16.13-0ubuntu0.24.04.1)
;     Dumped by pg_dump version: 16.13 (Ubuntu 16.13-0ubuntu0.24.04.1)

# 3) 空のDBを作って並列リストア
$ createdb -T template0 t59_restore
$ pg_restore -j 2 -d t59_restore t59_custom_zstd.dump   # 2.8秒、exit=0

# 4) 件数を確認
$ psql -d t59_restore -Atc "select (select count(*) from app.orders), (select count(*) from app.members), (select count(*) from archive.orders_2024), (select count(*) from app.access_logs)"
1000000|50000|100000|300000

createdb -T template0 にしているのは、template1 に誰かがオブジェクトを追加していると「already exists」の衝突が起きるためです。空のDBを作ってから戻すと、--clean なしでもエラーが出ません。

plain 形式のときは psql で戻します。-1(1トランザクション)と ON_ERROR_STOP=1 を付けると、途中で失敗したら何も残らない「全部か無しか」になります。

$ createdb -T template0 t59_copy
$ psql -X -q -1 -v ON_ERROR_STOP=1 -o /dev/null -d t59_copy -f t59_plain.sql   # exit=0
$ psql -d t59_copy -Atc "select count(*) from app.orders"
1000000

pg_restore で「一部だけ」戻す・DBごと作り直す

目次(-l)を編集して必要な行だけ戻す(-L)

# 目次から items のデータ行だけ抜き出す
$ pg_restore -l t59_custom.dump | grep -E "TABLE DATA.* items " > items.list
$ cat items.list
3493; 0 90298 TABLE DATA app items postgres

# 先にスキーマ(定義)を戻し、次に items のデータだけ戻す
$ pg_restore -s -d t59_copy t59_custom.dump
$ pg_restore -L items.list -d t59_copy t59_custom.dump
$ psql -d t59_copy -Atc "select (select count(*) from app.items) items, (select count(*) from app.orders) orders"
2000|0

目次ファイルは行頭に ; を付けるとその項目をスキップし、行を並べ替えると順序も変えられます。-t テーブル名 -n スキーマ名 で絞る方法もありますが、-L は「何を戻したか」がファイルに残るので、手順書に向いています。

-C でDBごと作り直す

$ dropdb t59
$ pg_restore -C -d postgres t59.dump

-C(--create)はアーカイブに記録されたDB名で CREATE DATABASE してから戻します。-d postgres は「最初の CREATE DATABASE を発行するための接続先」で、データは t59 に入ります。同名DBが残ったまま実行すると CREATE DATABASE が失敗し、そのまま既存DBへ流れ込もうとするので、先に dropdb するか --clean --create を付けます。

dropdb は接続が残っていると失敗します。--force で接続を切って削除できます(PostgreSQL 13以降)。

$ dropdb t59_copy
dropdb: error: database removal failed: ERROR:  database "t59_copy" is being accessed by other users
DETAIL:  There is 1 other session using the database.

$ dropdb --force t59_copy   # exit=0

よくあるエラー文と対処

実際に再現したエラー文と、直す前・直した後です。

エラー文(抜粋) 起きる場面 対処
ERROR: permission denied for table orders ダンプするロールに SELECT がない 読める権限を付ける。バックアップ用ロールに pg_read_all_data を付与
ERROR: permission denied for sequence items_item_id_seq テーブルは読めるが連番が読めない GRANT SELECT ON ALL SEQUENCES IN SCHEMA app TO ロール
ERROR: permission denied for database t59_restore 戻す先のDBに CREATE 権限がない ALTER DATABASE ... OWNER TO ロール か、所有者で実行
ERROR: role "t59_tmp_owner" does not exist ダンプ元のロールが戻す先にない 先に pg_dumpall --globals-only を流す、または --no-owner --no-privileges
ERROR: schema "app" already exists 中身があるDBに --clean なしで戻した --clean --if-exists を付けるか、空のDBに戻す
ERROR: permission denied to create extension "pageinspect" 非 trusted 拡張を非スーパーユーザーで戻した 拡張だけスーパーユーザーで先に CREATE EXTENSION、または --no-comments と目次で除外
fe_sendauth: no password supplied cron などパスワード入力できない場面 ~/.pgpass(0600)を用意する
pg_dump: error: aborting because of server version mismatch pg_dump より新しいメジャー版のサーバーに接続 サーバーと同じか新しい pg_dump を使う

permission denied(ダンプ側)

app.orders に SELECT がないロール t59_reader でDB全体を取ろうとした結果です。

$ pg_dump -h 127.0.0.1 -U t59_reader -d t59 -f /dev/null
pg_dump: error: query failed: ERROR:  permission denied for table orders
pg_dump: detail: Query was: LOCK TABLE app.members, app.items, app.orders, archive.orders_2024, app.access_logs IN ACCESS SHARE MODE

読めるテーブルだけに絞っても、連番(シーケンス)の現在値を読む権限が別に必要です。

$ pg_dump -h 127.0.0.1 -U t59_reader -d t59 -t app.members -t app.items -f reader_dump.sql
pg_dump: error: query failed: ERROR:  permission denied for sequence items_item_id_seq
pg_dump: detail: Query was: SELECT last_value, is_called FROM app.items_item_id_seq

直し方(どちらか):

-- A) 該当スキーマの連番を読めるようにする
GRANT SELECT ON ALL SEQUENCES IN SCHEMA app TO t59_reader;

-- B) バックアップ専用ロールを作り、全データ読み取りの定義済みロールを付ける(PostgreSQL 14以降)
CREATE ROLE t59_backup LOGIN PASSWORD '...';
GRANT pg_read_all_data TO t59_backup;

A の後は exit=0 で COPY app.items ... と COPY app.members ... が出力されました。B はスーパーユーザーを cron に使わずに済む方法で、後述のスクリプトで使っています。

permission denied(リストア側)と所有者

戻す先のDBで CREATE SCHEMA できないと止まります。

$ pg_restore -h 127.0.0.1 -U t59_reader -d t59_restore --exit-on-error t59_custom.dump
pg_restore: error: could not execute query: ERROR:  permission denied for database t59_restore
Command was: CREATE SCHEMA app;

直し方は、戻すロールをDBの所有者にし、ダンプ内の所有者・権限を無視することです。

$ psql -d postgres -c "ALTER DATABASE t59_restore OWNER TO t59_reader"
$ pg_restore -h 127.0.0.1 -U t59_reader -d t59_restore --no-owner --no-privileges t59_custom.dump   # exit=0
$ psql -d t59_restore -Atc "select tableowner from pg_tables where schemaname='app' and tablename='orders'"
t59_reader

role "..." does not exist

テーブルの所有者 t59_tmp_owner をダンプ後に削除し、別DBへ戻したときの出力です。

$ pg_restore -d t59_ext2 t59_ext_owner.dump
pg_restore: error: could not execute query: ERROR:  role "t59_tmp_owner" does not exist
Command was: ALTER TABLE public.t OWNER TO t59_tmp_owner;
pg_restore: warning: errors ignored on restore: 1

pg_restore は既定ではエラーを数えながら続行し、最後に errors ignored on restore: N を出して終了コード1になります。テーブルとデータ自体は入っていますが、所有者は実行ユーザーのままです。直し方は2つで、どちらも exit=0 を確認しました。

# A) 所有者を持ち込まない(開発環境向け)
pg_restore --no-owner --no-privileges -d t59_ext2 t59_ext_owner.dump   # 所有者は postgres になった

# B) 先にロールを作る(本番の移行向け)
psql -d postgres -f globals.sql          # pg_dumpall --globals-only の出力
pg_restore -d t59_ext2 t59_ext_owner.dump   # 所有者は t59_tmp_owner のまま復元

already exists(中身のあるDBに戻した)

データが入ったままのDBに --clean なしで戻すと、29件のエラーが出ました(主キー違反で COPY も失敗)。

$ pg_restore -d t59 t59_custom.dump
pg_restore: error: could not execute query: ERROR:  schema "app" already exists
Command was: CREATE SCHEMA app;
...
pg_restore: error: COPY failed for table "orders": ERROR:  duplicate key value violates unique constraint "orders_pkey"
pg_restore: warning: errors ignored on restore: 29

主キーがあったので元データは守られました(件数は100万行のまま)が、主キーのないテーブルだと行が二重に入ります。 上書きしたいときは --clean --if-exists を付けます。

$ pg_restore --clean --if-exists -d t59_restore t59_custom.dump   # exit=0、エラー0行、100万行

--if-exists なしの --clean を空のDBに使うと、今度は DROP が does not exist で29件のエラーになります。--clean と --if-exists は常にセットと覚えておくと、空のDBでも中身のあるDBでも同じコマンドで通ります。

拡張の権限(非 trusted 拡張)

pg_trgm や pgcrypto、hstore、btree_gist、uuid-ossp は trusted 拡張なので、DBの所有者なら CREATE EXTENSION できます(実際、t59_reader をDB所有者にしたリストアで pg_trgm は復元できました)。一方 pageinspect のような非 trusted 拡張を含むダンプを非スーパーユーザーで戻すと止まります。

$ pg_restore -h 127.0.0.1 -U t59_app -d t59_ext2 --no-owner t59_ext.dump
pg_restore: error: could not execute query: ERROR:  permission denied to create extension "pageinspect"
HINT:  Must be superuser to create this extension.
Command was: CREATE EXTENSION IF NOT EXISTS pageinspect WITH SCHEMA public;
pg_restore: warning: errors ignored on restore: 2

拡張が trusted かどうかは SELECT name, trusted FROM pg_available_extension_versions で確認できます。対処は「スーパーユーザーで先に CREATE EXTENSION してから戻す」か、目次(-l)から EXTENSION の行を ; でコメントにして -L で戻すことです。

サーバーのバージョン違い(未実行)

pg_dump は自分より新しいメジャー版のサーバーからは取れません(公式ドキュメント:refuse to even try)。この環境には16しかないため再現はしていませんが、ソースコード上のメッセージは次の形です。

pg_dump: error: aborting because of server version mismatch
pg_dump: detail: server version: 17.x; pg_dump version: 16.13

逆方向(古いサーバーを新しい pg_dump で取る)は 9.2 までサポートされ、メジャーバージョンアップの移行では「新しい側の pg_dump で古いサーバーから取る」が基本です。また、2025年8月のマイナーリリース(16.10 など)以降の pg_dump は plain 形式の先頭に \restrict という psql メタコマンドを出力します。それより前の psql で流すと invalid command になるので、クライアント側の psql も同じ時期以降に更新しておきます。

cron の日次バックアップ+世代管理スクリプト(実行確認)

custom+zstd で取り、目次が読めるか確認し、7日より古いファイルを消すスクリプトです。パスワードはスクリプトに書かず ~/.pgpass に置きます。

#!/bin/bash
# 日次バックアップ(カスタム形式・zstd圧縮)+世代管理
# 使い方: pg_backup.sh <DB名> [保存先] [保持日数]
set -euo pipefail

DB="${1:?DB名を指定してください}"
BACKUP_DIR="${2:-/var/backups/pg/$DB}"
KEEP_DAYS="${3:-7}"

export PGHOST="${PGHOST:-127.0.0.1}"
export PGUSER="${PGUSER:-t59_backup}"
# パスワードは ~/.pgpass(権限 0600)に書く。スクリプトには書かない

TS="$(date +%Y%m%d_%H%M%S)"
FILE="$BACKUP_DIR/${DB}_${TS}.dump"

mkdir -p "$BACKUP_DIR"
# 途中で失敗したら書きかけの .tmp を消す
trap 'rm -f "$FILE.tmp"' EXIT

echo "[$(date '+%F %T')] start  db=$DB file=$FILE"

# 1) 書き込み中は .tmp にし、成功したときだけ本来の名前にする
pg_dump -Fc --compress=zstd -d "$DB" -f "$FILE.tmp"
mv "$FILE.tmp" "$FILE"

# 2) ダンプの目次が読めるか確認(読めなければ壊れている)
pg_restore -l "$FILE" > /dev/null

# 3) 世代管理: KEEP_DAYS 日より古いダンプを削除
find "$BACKUP_DIR" -name "${DB}_*.dump" -type f -mtime +"$KEEP_DAYS" -print -delete \
  | sed 's/^/  deleted: /'

echo "[$(date '+%F %T')] done   size=$(du -h "$FILE" | cut -f1) kept=$(ls "$BACKUP_DIR"/${DB}_*.dump | wc -l)"

9日前・8日前・3日前の空ファイルを置いて実行しました。1回目は .pgpass を用意し忘れた状態です。

$ /tmp/t59_pg_backup.sh t59 /tmp/t59_backups 7 </dev/null
[2026-10-01 07:19:55] start  db=t59 file=/tmp/t59_backups/t59_20261001_071955.dump
pg_dump: error: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied
exit=1

$ printf "127.0.0.1:5432:*:t59_backup:t59backup\n" > ~/.pgpass && chmod 600 ~/.pgpass

$ /tmp/t59_pg_backup.sh t59 /tmp/t59_backups 7
[2026-10-01 07:19:55] start  db=t59 file=/tmp/t59_backups/t59_20261001_071955.dump
  deleted: /tmp/t59_backups/t59_20250922_023000.dump
  deleted: /tmp/t59_backups/t59_20250923_023000.dump
[2026-10-01 07:19:56] done   size=9.0M kept=2
exit=0

$ ls -l --time-style=+%F --block-size=K /tmp/t59_backups
-rw-rw-r-- 1 postgres postgres    0K 2026-09-28 t59_20250928_023000.dump
-rw-rw-r-- 1 postgres postgres 9120K 2026-10-01 t59_20261001_071955.dump

cron はパスワードを対話入力できないので、fe_sendauth: no password supplied が最も多いつまずきです。 .pgpass の書式は ホスト:ポート:DB:ユーザー:パスワード、権限は 0600 にします。crontab には次のように登録します(毎日 2:30、ログ追記)。

30 2 * * * /usr/local/bin/pg_backup.sh t59 /var/backups/pg/t59 7 >> /var/log/pg_backup.log 2>&1

運用でよく足す項目は、①別ホストやオブジェクトストレージへのコピー(rsync、aws s3 cp など)、②失敗時の通知(終了コードが0以外ならメールやチャットへ)、③月1回の「実際に戻す」テスト(次節)です。

リストア検証の手順表

バックアップは「取れた」ではなく「戻せた」で完了です。スクリプトで取った最新ファイルを検証用DBへ戻し、元DBと突き合わせました。

手順 コマンド/SQL 結果(今回)
1. 目次が読める pg_restore -l ファイル > /dev/null 終了コード0
2. 空のDBへ戻す createdb -T template0 t59_verify && pg_restore -j 2 -d t59_verify ファイル 終了コード0、errors ignored なし
3. 全テーブルの行数を比べる 下のSQLを両DBで実行し diff 5テーブルすべて一致
4. 主要テーブルのチェックサム SELECT md5(string_agg(t::text, ',' ORDER BY order_id)) FROM app.orders t 両方 c67dd03c6407feffe1ab5e8178aea96c
5. 連番の現在値 SELECT sequencename, last_value FROM pg_sequences WHERE schemaname='app' 4つとも一致(orders は 1000000)
6. オブジェクト数 pg_tables pg_indexes pg_views pg_proc の件数 5/9/1/1 で一致
7. アプリの代表クエリ SELECT * FROM app.order_summary ORDER BY total DESC LIMIT 2 ビュー・関数が動く
8. 統計情報を作る ANALYZE リストア直後は統計が空なので必ず実行

手順3で使った「全テーブルの正確な行数」を出すSQLです(information_schema から動的に数えます)。

SELECT table_schema, table_name,
       (xpath('/row/c/text()',
              query_to_xml(format('select count(*) as c from %I.%I', table_schema, table_name),
                           false, true, '')))[1]::text::bigint AS row_count
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
  AND table_type = 'BASE TABLE'
ORDER BY 1, 2;
 table_schema | table_name  | row_count
--------------+-------------+-----------
 app          | access_logs |    300000
 app          | items       |      2000
 app          | members     |     50000
 app          | orders      |   1000000
 archive      | orders_2024 |    100000

diff <(psql -d t59 -Atf verify.sql) <(psql -d t59_verify -Atf verify.sql) が空なら行数は一致です。手順8の ANALYZE は忘れやすいポイントで、これを飛ばすと復元直後のクエリが遅い実行計画を選ぶことがあります。

pg_basebackup/PITR との使い分け(概要)

pg_dump は「論理バックアップ」で、取った時点のスナップショット(1トランザクション内で一貫した状態)を戻せます。一方で、「障害の直前まで」戻したいときは物理バックアップ+WALアーカイブ(PITR:Point-In-Time Recovery)の出番です。

観点 pg_dump / pg_restore pg_basebackup + WALアーカイブ(PITR)
単位 DB単位(テーブル単位も可) クラスタ全体(全DB)
戻せる時点 ダンプを取った時点 WALがある範囲の任意の時点(recovery_target_time)
版・OSをまたぐ移行 可(新しい版へ読み込める) 同じメジャー版・同じ構成が前提
サイズ・時間 圧縮が効き小さい。大きいDBは時間がかかる データディレクトリ相当。取得は速い
準備 不要 wal_level=replica、archive_mode=on、archive_command の設定
向く場面 日次バックアップ、環境コピー、移行、テーブルの取り違え復旧 RPO(失ってよい時間)が短い本番

最小の流れは、pg_basebackup -D /backup/base -Fp -Xs -P でベースを取り、archive_command でWALを保管し、復旧時に restore_command と recovery_target_time を設定して recovery.signal を置く、というものです。多くの現場では「日次の pg_dump + 本番だけ PITR」の二段構えにすると、開発環境へのコピーと本番の直前復旧の両方を満たせます。PostgreSQL 17 では pg_basebackup に増分バックアップ(--incremental)が加わっています。

よくある質問

Q. pg_dump はテーブルをロックしますか?サービス中に取っても大丈夫ですか? A. 取得中は各テーブルに ACCESS SHARE ロックを取ります。通常の SELECT/INSERT/UPDATE/DELETE は妨げず、ALTER TABLE や DROP TABLE などのDDLとだけ競合します。ダンプ全体は1つのトランザクション(-j のときは同期スナップショット)で一貫した状態になります。

Q. custom と directory、どちらを常用すべきですか? A. ファイル1つで扱いやすい custom が基本です。テーブル数が多くダンプ・リストアの時間が問題になったら、並列ダンプができる唯一の形式である directory に切り替えます。どちらも pg_restore で戻し、-l/-L の部分リストアや -j の並列リストアが使えます。

Q. pg_dump のファイルは別のメジャーバージョンに戻せますか? A. 新しい版へは戻せます(公式:新しいサーバーへ読み込めることが期待できる)。取るときは新しい側の pg_dump を使い、予約語の差を避けたいときは --quote-all-identifiers を付けます。古い版の pg_restore で新しい版のダンプを読むことはできません。

Q. リストアが遅いとき、まず何を見ればよいですか? A. ①-j を CPU コア数まで上げる(custom/directory のみ)、②戻す先の maintenance_work_mem を一時的に上げてインデックス作成を速くする、③--section=pre-data → data → post-data の順に分けて、データ投入の後でインデックスを作る、の順に試します。plain 形式なら -Fc で取り直すのが最も効きます。

まとめ

  • 形式は4つ。迷ったら -Fc --compress=zstd、テーブルが多いなら -Fd -j。 tar は圧縮不可、plain は psql で戻す
  • 並列ダンプは directory 形式だけ、並列リストアは custom/directory。 -1 と -j は同時に使えない
  • 実測では zstd が gzip より速く、サイズは約6割(PostgreSQL 16 で lz4/zstd に対応)
  • 空のDB(createdb -T template0)に戻すのが基本。 上書きなら --clean --if-exists を常にセットで
  • permission denied は連番の権限、role does not exist は --globals-only か --no-owner で解決する
  • cron の失敗の定番は .pgpass 忘れ。 スクリプトは .tmp → mv、目次確認、find -mtime +N -delete の3点セット
  • 「戻せた」までがバックアップ。 行数・チェックサム・連番・ANALYZE の手順表で検証する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「pg_dump/pg_restoreの使い方|バックアップとリストアの実行例」

株式会社RJCでは、バックアップやリストアのように「地味だけれど確実に動いてほしい」運用の仕組みも、実際に手を動かして確かめながら仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?