PostgreSQL の管理コマンド一覧(DBA)

PostgreSQL DBA コースで覚える構文の一覧です。共通 SQL コースの構文(SELECT・JOIN・GROUP BY・ウィンドウ関数など)はすべて使えます。

CREATE ROLEロールを作る(PostgreSQL)Lv1 で覚える

CREATE ROLE 名前 [LOGIN] [PASSWORD 'パスワード'] [CREATEDB] [CREATEROLE];
CREATE USER 名前 …;(LOGIN 付きの CREATE ROLE)
ALTER ROLE 名前 属性;
DROP ROLE 名前;

PostgreSQL では、ユーザーもグループも「ロール」です。LOGIN があるロールだけが接続できます(CREATE USER は LOGIN が最初から付いた CREATE ROLE)。属性は ALTER ROLE で後から変えられます。持ち物(表など)があるロールは DROP できません。

CREATE ROLE clerk LOGIN PASSWORD 'zou-2026';
ALTER ROLE clerk CREATEDB;
DROP ROLE clerk;

\duロールの一覧を見る(psql)Lv2 で覚える

\du
\du 名前
\dg

ロールと属性(Superuser・Create role・Create DB・Cannot login・Password valid until など)を一覧にします。何も書かれていないロールは、ログインできるふつうのロールです。pg_roles を SELECT しても同じことが分かります。

\du
SELECT rolname, rolcanlogin FROM pg_roles WHERE rolname !~ '^pg_';

\c と psql -U別のロールで接続するLv3 で覚える

\c データベース ロール [ホスト] [ポート]
\! psql -h localhost -U ロール -d quest -c "SQL"
\! PGPASSWORD=パスワード psql -h ホスト -U ロール -c "SQL"
\! ssh app01 psql -h db01 -U ロール -c "SQL"

\c は今の psql の接続を新しい相手に替えます(省いたものや - は前の接続と同じ)。psql -c は 1 回だけ接続して SQL を実行します。どちらも pg_hba.conf の規則で許されて、パスワードなどの認証に通ったときだけ接続できます。パスワードが要るときは Password for user …: と聞かれます。このシミュレーターでは quest データベースにだけ接続できます。

\c quest clerk localhost
\! psql -h localhost -U clerk -c "SELECT current_user"

GRANT表や列の権限を与える(PostgreSQL)Lv4 で覚える

GRANT SELECT, INSERT ON 表 TO ロール;
GRANT SELECT (列, 列) ON 表 TO ロール;
GRANT UPDATE (列) ON 表 TO ロール;
GRANT SELECT ON ALL TABLES IN SCHEMA スキーマ TO ロール;
GRANT ロール TO ロール;

表の持ち主とスーパーユーザー以外は、権限がないと permission denied になります。列を書くと、その列だけの権限になります。ALL TABLES IN SCHEMA は「今ある表」だけに与えます(これから作る表は ALTER DEFAULT PRIVILEGES)。ロールをロールに GRANT すると、メンバーにできます。

GRANT SELECT ON scrolls TO clerk;
GRANT UPDATE (stock) ON scrolls TO clerk;

SET ROLEほかのロールになって試すLv5 で覚える

SET ROLE ロール;
RESET ROLE;
SELECT current_user, session_user;

スーパーユーザーや、そのロールのメンバーは、SET ROLE でそのロールに切り替えられます。権限を正しく与えたか、本人になって試すときに便利です。current_user が今のロール、session_user は接続したロールです。RESET ROLE で戻ります。

SET ROLE clerk;
SELECT * FROM scrolls;
RESET ROLE;

REVOKE権限を取り消す(PostgreSQL)Lv6 で覚える

REVOKE 権限 ON 表 FROM ロール;
REVOKE ALL ON 表 FROM ロール;
REVOKE 権限 ON 表 FROM PUBLIC;
REVOKE GRANT OPTION FOR 権限 ON 表 FROM ロール;

GRANT で与えた権限を取り消します。PUBLIC は「すべてのロール」のことで、PUBLIC に与えた権限は、そのロールから取り消しても残ります。WITH GRANT OPTION で渡した「人に渡す権利」は GRANT OPTION FOR で取り消します。

REVOKE INSERT ON scrolls FROM clerk;
REVOKE ALL ON scrolls FROM PUBLIC;

スキーマの権限スキーマに入る権利と、作る権利Lv7 で覚える

GRANT USAGE ON SCHEMA スキーマ TO ロール;
GRANT CREATE ON SCHEMA スキーマ TO ロール;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
CREATE SCHEMA スキーマ AUTHORIZATION ロール;
\dn+

スキーマの USAGE がないと、中の表に権限があっても使えません。CREATE があると、そのスキーマに表を作れます。PostgreSQL 15 から public スキーマの CREATE は PUBLIC に与えられていませんが、古い版から上げたデータベースでは残っていることがあります。\dn+ の Access privileges で確かめます(U が USAGE、C が CREATE)。

REVOKE CREATE ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA archive TO clerk;

ALTER DEFAULT PRIVILEGESこれから作る表の権限を決めるLv9 で覚える

ALTER DEFAULT PRIVILEGES IN SCHEMA スキーマ GRANT SELECT ON TABLES TO ロール;
ALTER DEFAULT PRIVILEGES FOR ROLE 作る人 IN SCHEMA スキーマ GRANT …;
\ddp

GRANT は今ある表にだけ効きます。これから作る表にも自動で権限を付けたいときは ALTER DEFAULT PRIVILEGES を使います。決まりは「表を作るロール」ごとで、FOR ROLE を省くと自分が作る表の決まりになります。\ddp で確かめます。

ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readers;

\dp と has_table_privilege誰が何をできるかを確かめるLv10 で覚える

\dp [表]
\z [表]
SELECT has_table_privilege('ロール', '表', 'SELECT');
SELECT has_column_privilege('ロール', '表', '列', 'UPDATE');

\dp の Access privileges は「ロール=権限/与えた人」の形です。r=SELECT、a=INSERT、w=UPDATE、d=DELETE、D=TRUNCATE、x=REFERENCES、t=TRIGGER、m=MAINTAIN、* は人に渡せる権利。空のときは、持ち主だけが使える既定の状態です。has_…_privilege は true / false で答えます。

\dp scrolls
SELECT has_table_privilege('clerk', 'scrolls', 'INSERT');

行レベルセキュリティ行ごとに見せる・見せないを決めるLv11 で覚える

ALTER TABLE 表 ENABLE ROW LEVEL SECURITY;
CREATE POLICY 名前 ON 表 [FOR SELECT|INSERT|UPDATE|DELETE] [TO ロール] USING (条件) [WITH CHECK (条件)];
DROP POLICY 名前 ON 表;

RLS を有効にすると、ポリシーの USING の条件に合う行だけが見え、WITH CHECK の条件に合う行だけを書けます。ポリシーが 1 つもなければ何も見えません。表の持ち主とスーパーユーザーは RLS を素通りします(持ち主にも効かせるには FORCE ROW LEVEL SECURITY)。

ALTER TABLE letters ENABLE ROW LEVEL SECURITY;
CREATE POLICY own_letters ON letters USING (owner = current_user);

pg_hba.conf誰がどこから接続できるかLv13 で覚える

TYPE  DATABASE  USER  ADDRESS  METHOD
local  all  all  peer
host  quest  app  192.168.10.0/24  scram-sha-256
SELECT * FROM pg_hba_file_rules;

接続が来ると、上の行から順に TYPE(local はソケット、host は TCP/IP)・DATABASE・USER・ADDRESS を比べ、最初に合った行の METHOD で認証します。合う行がなければ no pg_hba.conf entry で断られます。METHOD は trust(パスワードなし)・reject(断る)・scram-sha-256・md5・peer(OS のユーザーと同じ名前か)など。書き換えたら再読み込みが要ります。pg_hba_file_rules でファイルの読み方と間違い(error)を確かめられます。

\! cat pg_hba.conf
\! echo 'host quest app 192.168.10.21/32 scram-sha-256' >> pg_hba.conf

設定の再読み込み設定ファイルを読み直させるLv14 で覚える

\! pg_ctl reload
SELECT pg_reload_conf();

pg_hba.conf や postgresql.conf は、書き換えただけでは効きません。サーバーに再読み込みさせると、pg_hba.conf の規則と、再起動が要らない設定が変わります。接続はそのまま続きます。pg_hba.conf に間違いがあると、前の規則のまま(ログに pg_hba.conf was not reloaded)です。

\! pg_ctl reload
SELECT pg_reload_conf();

SHOW と pg_settings設定の値を見るLv16 で覚える

SHOW 名前;
SELECT current_setting('名前');
SELECT name, setting, unit, context, source FROM pg_settings WHERE name = '名前';
SET 名前 = 値;(そのセッションだけ)

pg_settings の context は、変えたものがいつ効くかを表します。postmaster は再起動、sighup は再読み込み、superuser と user は SET でそのセッションだけ変えられます。source はどこで決まったか(default・configuration file など)、pending_restart は再起動を待っている設定です。

SHOW max_connections;
SELECT name, setting, context FROM pg_settings WHERE name = 'work_mem';

postgresql.confサーバーの設定ファイルLv17 で覚える

名前 = 値    # コメント
\! grep -n 名前 postgresql.conf
\! sed -i "s/^#名前 = .*/名前 = 値/" postgresql.conf
SELECT name, setting, applied, error FROM pg_file_settings;

データディレクトリ($PGDATA)にある設定ファイルです。# で始まる行はコメントで、その設定は既定値のままです。同じ名前を何度も書くと最後の行が勝ちます。書き換えたら再読み込み(再起動が要る設定は再起動)で効きます。pg_file_settings でファイルに書いた値と、効いているか(applied)を確かめられます。

\! grep -n work_mem postgresql.conf
\! sed -i "s/^#work_mem = 4MB/work_mem = 8MB/" postgresql.conf

ALTER SYSTEMSQL で設定ファイルを書き換えるLv18 で覚える

ALTER SYSTEM SET 名前 = 値;
ALTER SYSTEM RESET 名前;
ALTER SYSTEM RESET ALL;

ALTER SYSTEM は postgresql.auto.conf に書きます。このファイルは postgresql.conf の後に読まれるので、同じ設定なら auto.conf が勝ちます。書いただけでは効かないので、再読み込み(再起動が要る設定は再起動)します。トランザクションの中では使えません。auto.conf は手で書き換えないでください。

ALTER SYSTEM SET work_mem = '16MB';
SELECT pg_reload_conf();

pg_ctlサーバーを起動・停止・再起動するLv19 で覚える

\! pg_ctl status
\! pg_ctl stop [-m fast|smart|immediate]
\! pg_ctl start
\! pg_ctl restart [-m fast]

データディレクトリは環境変数 PGDATA で決まります(-D で書いてもよい)。stop の既定は fast(接続を切って、きれいに止める)。smart は接続がなくなるまで待ち、immediate はすぐに落とします(次の起動で回復の処理が走る)。止めると psql の接続も切れ、次の SQL で psql がつなぎ直します(Attempting reset)。

\! pg_ctl status
\! pg_ctl restart

サーバーのログサーバーで起きたことを読むLv21 で覚える

\! tail -n 20 log/postgresql.log
\! grep FATAL log/postgresql.log
ALTER SYSTEM SET log_connections = 'on';
ALTER SYSTEM SET log_min_duration_statement = '100ms';

logging_collector が on だと、ログは $PGDATA の log_directory(log)の log_filename に書かれます。接続に失敗した本当の理由(パスワードが違う・期限切れなど)は、相手には見せずにログの DETAIL にだけ出ます。log_connections・log_disconnections で接続を、log_min_duration_statement で遅い SQL を記録できます。エラーになった SQL は STATEMENT に出ます。

\! tail -n 5 log/postgresql.log
\! grep DETAIL log/postgresql.log

MVCC と不要な行UPDATE・DELETE は古い行を残すLv23 で覚える

SELECT ctid, xmin, xmax, * FROM 表;
SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables;

PostgreSQL は行を書き換えず、新しい版の行を足します(MVCC)。xmin はその行を作ったトランザクション、xmax は消したトランザクション、ctid は行の場所です。古い版は、見ている人がいなくなっても「不要な行(dead tuple)」として残ります。DELETE や、ROLLBACK した UPDATE も不要な行を作ります。数は pg_stat_user_tables の n_dead_tup で見られます。

SELECT ctid, xmin, xmax, id FROM treasure WHERE id = 1;
SELECT relname, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;

VACUUM不要な行を掃除するLv24 で覚える

VACUUM 表;
VACUUM (VERBOSE) 表;
VACUUM;(すべての表)

不要な行の場所を、また使えるようにします。表のファイルは小さくなりません(末尾が空いたときだけ切り詰めます)。表を止めずに動くので、いつでも実行できます。VERBOSE を付けると、消した行の数(tuples: … removed)などが INFO に出ます。古いトランザクションが残っていると、dead but not yet removable(まだ消せない)になります。

VACUUM (VERBOSE) treasure;
SELECT last_vacuum, vacuum_count FROM pg_stat_user_tables WHERE relname = 'treasure';

ANALYZE と pg_stats実行計画のための統計を集めるLv25 で覚える

ANALYZE 表;
VACUUM ANALYZE 表;
SELECT attname, n_distinct, most_common_vals FROM pg_stats WHERE tablename = '表';
ALTER TABLE 表 ALTER COLUMN 列 SET STATISTICS 数;

ANALYZE は表の中身を調べて、値の種類や多い値の割合を pg_stats に残します。プランナーはこれで行の数を見積もり、Seq Scan か Index Scan かを選びます。データが大きく変わったのに統計が古いと、見積もり(rows=)が外れて遅い計画を選びます。SET STATISTICS で、調べる細かさを列ごとに変えられます。

ANALYZE orders;
EXPLAIN SELECT * FROM orders WHERE status = 'new';

autovacuum自動の掃除と統計Lv26 で覚える

SHOW autovacuum;
SELECT name, setting FROM pg_settings WHERE name LIKE 'autovacuum%';
ALTER TABLE 表 SET (autovacuum_vacuum_scale_factor = 0.01);
ALTER TABLE 表 RESET (autovacuum_enabled);

autovacuum は 1 分ごと(autovacuum_naptime)に表を見回り、不要な行が「autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × 行の数」を超えた表を VACUUM、変わった行が多い表を ANALYZE します。大きな表は割合のしきい値に届きにくいので、表ごとに小さくします。結果は pg_stat_user_tables の last_autovacuum・autovacuum_count で確かめます。このシミュレーターでは、\! sleep 60 で 1 分が経ちます。

ALTER TABLE big_log SET (autovacuum_vacuum_scale_factor = 0.01);
\! sleep 60

表の大きさディスクをどれだけ使っているかLv27 で覚える

SELECT pg_size_pretty(pg_relation_size('表'));
SELECT pg_size_pretty(pg_total_relation_size('表'));
SELECT pg_size_pretty(pg_indexes_size('表'));

pg_relation_size は表そのもの、pg_indexes_size はインデックスの合計、pg_total_relation_size は表・インデックス・TOAST の合計です(バイト)。pg_size_pretty で kB・MB の形にします。不要な行が多い表は、行の数のわりに大きくなります(ふくらみ)。

SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC;

VACUUM FULL表を作り直して縮めるLv28 で覚える

VACUUM FULL 表;
VACUUM (FULL, VERBOSE) 表;
\! df -h $PGDATA

表を新しいファイルに書き直して、空いた場所を OS に返します(relfilenode が変わります)。その間は ACCESS EXCLUSIVE ロックを持つので、読むこともできません。ほかの接続がその表を使っていると、終わるまで待ちます。新しいファイルを書き終えてから古いファイルを消すので、作り直した表の大きさの分だけ空きディスクも要ります(df -h で先に確かめる。足りないと No space left on device で止まります)。止められない表では、ふつうの VACUUM をこまめに動かすほうが大事です。

VACUUM FULL treasure;
SELECT relfilenode FROM pg_class WHERE relname = 'treasure';

REINDEXインデックスを作り直すLv29 で覚える

REINDEX INDEX インデックス;
REINDEX TABLE 表;
REINDEX INDEX CONCURRENTLY インデックス;

インデックスも、更新を重ねるとふくらみます。REINDEX は作り直して小さくします。ふつうの REINDEX は表への書き込みを止めます。CONCURRENTLY を付けると、書き込みを止めずに作り直せます(時間はかかります)。失敗した CONCURRENTLY の作業は、pg_index の indisvalid が false の INVALID なインデックスとして残ります。

REINDEX INDEX CONCURRENTLY orders_updated_idx;
SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;

凍結と XID の年齢トランザクション ID の周回を防ぐLv30 で覚える

SELECT datname, age(datfrozenxid) FROM pg_database;
SELECT relname, age(relfrozenxid) FROM pg_class WHERE relkind = 'r';
VACUUM FREEZE 表;

トランザクション ID(XID)は約 21 億で一周します。古い行は「凍結」して、どの XID よりも古いと印を付けておく必要があります。age(relfrozenxid) は、その表でまだ凍結していない一番古い XID からの年齢です。autovacuum_freeze_max_age(既定 2 億)を超えると、autovacuum を止めていても周回を防ぐ VACUUM が動きます。VACUUM FREEZE はすぐに凍結します。

VACUUM FREEZE old_ledger;
SELECT age(datfrozenxid) FROM pg_database WHERE datname = 'quest';

vacuumdbデータベースをまとめて掃除するLv31 で覚える

\! vacuumdb -z quest
\! vacuumdb -Z quest
\! vacuumdb -t 表 -v quest
\! vacuumdb -F quest

シェルから VACUUM を流すコマンドです。cron で夜中に動かすのによく使います。-z は VACUUM と ANALYZE、-Z は ANALYZE だけ、-F は FREEZE、-f は FULL、-t は表を選び、-v は詳しく表示します。

\! vacuumdb -z quest

pg_dumpデータベースを SQL の形で写す(論理バックアップ)Lv33 で覚える

\! pg_dump -f ファイル.sql quest
\! pg_dump -Fc -f ファイル.dump quest
\! pg_dump -t 表 -f ファイル.sql quest
\! pg_dump -n スキーマ -s -f ファイル.sql quest

動いているデータベースから、表の定義(CREATE TABLE)と行(COPY)を取り出します。取っている間も、ほかの人は読み書きできます。-Fp(既定)は読める SQL のテキスト、-Fc はカスタム形式(圧縮され、pg_restore で表を選んで戻せる)。-t は表、-n はスキーマを選び、-s(--schema-only)は定義だけ、-a(--data-only)は行だけを写します。ロールは写りません(pg_dumpall -g)。

\! pg_dump -Fc -f /backup/quest.dump quest
\! grep COPY /backup/quest.sql

pg_restore と psql -fダンプから戻すLv34 で覚える

\! pg_restore -l ファイル.dump
\! pg_restore -d quest ファイル.dump
\! pg_restore -d quest -t 表 ファイル.dump
\! pg_restore --clean -d quest ファイル.dump
\! psql -f ファイル.sql

カスタム形式は pg_restore で戻します。-l は中身の一覧(TOC)、-d は戻す先のデータベース、-t は表だけ(インデックス・制約・権限は戻りません)、-a は行だけ、--clean は先に DROP してから作り直します(--if-exists で、無いものの DROP をエラーにしない)。テキスト形式は psql -f で流します。どちらもエラーがあっても続け、最後に数を出します。

\! pg_restore -d quest -t treasure /backup/quest.dump
\! psql -f /backup/quest.sql

pg_dumpallロール(クラスター全体のもの)も写すLv35 で覚える

\! pg_dumpall --globals-only -f ファイル.sql
\! pg_dumpall -f ファイル.sql
\! psql -f ファイル.sql

ロールやロールのメンバーは、データベースではなくクラスター全体のものなので、pg_dump には入りません。pg_dumpall --globals-only(-g)はロールの定義(CREATE ROLE と ALTER ROLE … PASSWORD)だけを写します。表と一緒に別のサーバーへ移すときは、先にロールを戻してから表を戻します(GRANT の相手が無いとエラーになる)。スーパーユーザーで実行します。

\! pg_dumpall -g -f /backup/globals.sql
\! psql -f /backup/globals.sql

WALすべての変更の記録Lv36 で覚える

SELECT pg_current_wal_lsn();
SELECT pg_walfile_name(pg_current_wal_lsn());
SELECT pg_wal_lsn_diff(LSN, LSN);
SELECT pg_switch_wal();
SELECT name, size FROM pg_ls_waldir();

PostgreSQL は、データを書き換える前に、変更を WAL(write-ahead log)に書きます。WAL の位置は LSN(0/3000148 のような形)で表し、16MB ごとのファイル(000000010000000000000003)に分かれて $PGDATA/pg_wal にたまります。pg_wal_lsn_diff は 2 つの位置の差(バイト)、pg_switch_wal は今のファイルを閉じて次のファイルへ進めます。

SELECT pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn());
\! ls pg_wal

WAL のアーカイブ書き終わった WAL を別の場所に保管するLv37 で覚える

ALTER SYSTEM SET archive_mode = on;(再起動が要る)
ALTER SYSTEM SET archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f';
SELECT * FROM pg_stat_archiver;

archive_mode を on にすると、WAL のファイルが書き終わるたびに archive_command を実行します。%p はファイルのパス、%f はファイルの名前です。test ! -f で、同じ名前を上書きしないようにします。コマンドが失敗すると、成功するまでやり直し、その間 WAL は pg_wal にたまり続けます。pg_stat_archiver の failed_count・last_failed_wal と、ログの archive command failed で気づけます。

SELECT archived_count, last_archived_wal, failed_count FROM pg_stat_archiver;

pg_basebackupデータディレクトリを丸ごと写す(物理バックアップ)Lv38 で覚える

\! pg_basebackup -D ディレクトリ -Ft -X stream -P
\! PGPASSWORD=… pg_basebackup -h localhost -U ロール -D ディレクトリ
\! pg_basebackup -D ディレクトリ -Fp -R

動いているサーバーから、レプリケーションの接続でデータディレクトリを写します。-Ft は tar(base.tar と pg_wal.tar)、-Fp はそのままのディレクトリ。-X stream は取っている間の WAL も一緒に写し、これだけで戻せるバックアップになります。-P は進み具合を出し、-R はスタンバイ用の設定(standby.signal と primary_conninfo)を書きます。REPLICATION の属性と、pg_hba.conf の replication の行が要ります。

\! pg_basebackup -D /backup/base -Ft -X stream -P

REPLICATION の属性レプリケーションの接続を許すLv39 で覚える

CREATE ROLE 名前 LOGIN REPLICATION PASSWORD '…';
ALTER ROLE 名前 REPLICATION;

pg_basebackup やスタンバイは、レプリケーションの接続でサーバーにつなぎます。それにはスーパーユーザーか、REPLICATION の属性を持つロールが要ります(無いと permission denied to start WAL sender)。バックアップやスタンバイのために、スーパーユーザーではない専用のロールを作るのが安全です。

CREATE ROLE backup LOGIN REPLICATION PASSWORD 'shelf-key';

PITR(時点を指定した復旧)ベースバックアップと WAL で、ある時点に戻すLv40 で覚える

\! tar -xf /backup/base/base.tar -C $PGDATA
restore_command = 'cp /backup/wal/%f %p'
recovery_target_time = '2026-11-04 22:14:00'
recovery_target_name = '名前'(pg_create_restore_point)
\! touch $PGDATA/recovery.signal
SELECT pg_wal_replay_resume();

止めたサーバーのデータディレクトリをベースバックアップで置き換え、restore_command(アーカイブから WAL を取ってくるコマンド)と目標(recovery_target_time・_name・_lsn)を書き、recovery.signal を置いて起動します。サーバーは WAL を流し直して目標で止まり(recovery_target_action の既定は pause)、読むだけの状態になります。データを確かめてから pg_wal_replay_resume() で復旧を終えると、新しいタイムラインで書けるようになります。

\! touch recovery.signal
SELECT pg_is_in_recovery();
SELECT pg_wal_replay_resume();

pg_verifybackup とタイムラインバックアップが使えるか・復旧のあとを確かめるLv41 で覚える

\! pg_verifybackup /backup/base
SELECT timeline_id FROM pg_control_checkpoint();
\! cat /backup/wal/00000002.history

pg_verifybackup は backup_manifest と見比べて、ファイルが欠けたり壊れたりしていないか、必要な WAL があるかを確かめます。復旧で書けるようになると、タイムラインの番号が 1 つ増え、どこで分かれたかを 0000000N.history に書きます。復旧が目標に届かないと、ログに recovery ended before configured recovery target was reached と出て起動しません。

\! pg_verifybackup /backup/base

pg_archivecleanup要らなくなった古い WAL を消すLv42 で覚える

\! pg_archivecleanup アーカイブの場所 一番古く残すファイル
\! pg_archivecleanup -n /backup/wal 000000010000000000000005

ベースバックアップより前の WAL は、そのバックアップから戻すときには要りません。いちばん古く残すベースバックアップの .backup ファイル(START WAL LOCATION)を見て、それより前の WAL のファイルを消します。-n は消さずに一覧だけ出します。新しいバックアップを取って確かめてから消します。

\! cat /backup/wal/000000010000000000000005.00000028.backup

pg_stat_activityいまの接続と、何をしているかLv44 で覚える

SELECT pid, usename, state, wait_event_type, wait_event, query FROM pg_stat_activity;
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;

接続ごとに 1 行です。state は active(実行中)・idle(待ち)・idle in transaction(トランザクションの途中で止まっている)。query_start は今の問い合わせの開始、xact_start はトランザクションの開始、wait_event_type・wait_event は何を待っているか(Lock ならロック、Client ならアプリの返事)。backend_type が client backend の行がアプリなどの接続です。

SELECT pid, now() - xact_start AS xact_age, query FROM pg_stat_activity WHERE state = 'idle in transaction';

pg_cancel_backend と pg_terminate_backend暴走した問い合わせを止めるLv45 で覚える

SELECT pg_cancel_backend(pid);
SELECT pg_terminate_backend(pid);
ALTER SYSTEM SET idle_in_transaction_session_timeout = '10min';

pg_cancel_backend は今の問い合わせだけを取り消します(接続は残ります。何も実行していない接続には効きません)。pg_terminate_backend は接続ごと切ります(トランザクションは ROLLBACK されます)。idle_in_transaction_session_timeout を決めておくと、トランザクションの途中で止まった接続を自動で切ります(効くのは決めたあとに止まった接続です。いま止まっている接続は、読み直しても切られないので手で切ります)。

SELECT pg_cancel_backend(5103);
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction';

pg_locks と pg_blocking_pidsロックの待ちを調べるLv46 で覚える

SELECT pid, relation::regclass, mode, granted FROM pg_locks WHERE NOT granted;
SELECT pid, pg_blocking_pids(pid) FROM pg_stat_activity WHERE wait_event_type = 'Lock';
SET lock_timeout = '3s';

pg_locks はロックの一覧です。granted が false の行は、取れずに待っているロックです。pg_blocking_pids(pid) は、その接続を待たせている接続の pid の配列を返します。待たせている元(多くは idle in transaction)を見つけて終わらせます。DDL を流すときは lock_timeout を決めておくと、長く待たずにあきらめます。

SELECT pid, state, query FROM pg_stat_activity WHERE pid = ANY(pg_blocking_pids(5202));

pg_stat_statements重い SQL を探すLv47 で覚える

ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';(再起動が要る)
CREATE EXTENSION pg_stat_statements;
SELECT query, calls, total_exec_time, mean_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;
SELECT pg_stat_statements_reset();

SQL の形(値は $1 に置き換えたもの)ごとに、回数(calls)・合計の時間(total_exec_time)・1 回の平均(mean_exec_time)・読んだブロックなどを集めます。サーバーの起動時に読み込む必要があり(shared_preload_libraries)、データベースに CREATE EXTENSION します。total_exec_time の大きいものから直すと、効き目が大きくなります。

SELECT query, calls, round(mean_exec_time::numeric, 2) FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 3;

EXPLAIN (ANALYZE, BUFFERS)本当に実行して計画を測るLv48 で覚える

EXPLAIN (ANALYZE, BUFFERS) SELECT …;
BEGIN; EXPLAIN (ANALYZE) UPDATE …; ROLLBACK;

実際に実行して、ノードごとの見積もり(cost・rows)と実際(actual time・rows)、読んだブロック(Buffers: shared hit はキャッシュ、read はディスク)を見せます。見積もりと実際の行の数が大きく違うときは、統計が古いことが多いです。UPDATE などは本当に書き換えるので、BEGIN と ROLLBACK で包みます。

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id = 42;

pg_stat_user_indexes使われていないインデックスを探すLv49 で覚える

SELECT indexrelname, idx_scan FROM pg_stat_user_indexes ORDER BY idx_scan;
DROP INDEX CONCURRENTLY インデックス;

idx_scan はそのインデックスを使った回数です。長い間 0 のインデックスは、書き込みを遅くし、ディスクを使うだけです。ただし主キーや UNIQUE のインデックスは、重複を防ぐ役目があるので消しません。統計を消した(pg_stat_reset)直後や、月に一度の処理だけが使うものにも気をつけます。

SELECT s.indexrelname FROM pg_stat_user_indexes s JOIN pg_index i ON i.indexrelid = s.indexrelid WHERE s.idx_scan = 0 AND NOT i.indisunique;

work_mem と一時ファイル並べ替えがディスクにあふれていないかLv50 で覚える

EXPLAIN (ANALYZE) SELECT … ORDER BY …;
SELECT temp_files, temp_bytes FROM pg_stat_database WHERE datname = 'quest';
ALTER SYSTEM SET log_temp_files = 0;
\! free -h

並べ替えやハッシュは work_mem までメモリを使い、超えると一時ファイルに書きます(Sort Method: external merge Disk: …)。pg_stat_database の temp_files・temp_bytes で数を、log_temp_files でログを確かめます。work_mem は 1 つの並べ替えごとに使うので、大きくしすぎると接続が多いときにメモリが足りなくなります。free でサーバーのメモリを見ます。

SET work_mem = '64MB';
ALTER ROLE report SET work_mem = '64MB';

キャッシュのヒット率shared_buffers が足りているかLv51 で覚える

SELECT blks_hit, blks_read, round(100.0 * blks_hit / (blks_hit + blks_read), 2) FROM pg_stat_database WHERE datname = 'quest';
SELECT relname, heap_blks_hit, heap_blks_read FROM pg_statio_user_tables;

blks_hit は shared_buffers(キャッシュ)で見つかったブロック、blks_read はキャッシュに無く読みに行ったブロックです。業務のシステムでは 99% 以上が目安です。低いときは、よく読むデータが shared_buffers に入りきっていません(メモリの 25% くらいが目安。変えるには再起動が要ります)。

SELECT relname, round(100.0 * heap_blks_hit / NULLIF(heap_blks_hit + heap_blks_read, 0), 1) FROM pg_statio_user_tables;

チェックポイント書き込みの山をならすLv52 で覚える

SELECT num_timed, num_requested FROM pg_stat_checkpointer;
ALTER SYSTEM SET max_wal_size = '16GB';
ALTER SYSTEM SET log_checkpoints = on;
SELECT pg_stat_reset_shared('checkpointer');

チェックポイントは、メモリの変更をデータファイルに書き出す処理です。checkpoint_timeout(既定 5 分)ごとに動くもの(num_timed)と、WAL が max_wal_size に近づいて急いで動くもの(num_requested)があります。num_requested が多いと書き込みが集中し、ログに checkpoints are occurring too frequently と出ます。max_wal_size を大きくすると、時間で動くようになります。

SELECT num_timed, num_requested FROM pg_stat_checkpointer;

wal_level と max_wal_sendersレプリケーションの準備Lv54 で覚える

SHOW wal_level;
ALTER SYSTEM SET wal_level = replica;(再起動が要る)
ALTER SYSTEM SET max_wal_senders = 10;

wal_level は WAL にどれだけ書くか(minimal・replica・logical)です。スタンバイや pg_basebackup には replica 以上、論理レプリケーションには logical が要ります。max_wal_senders は、WAL を送る接続の最大の数です。どちらも再起動で変わります。

SELECT name, setting, context FROM pg_settings WHERE name IN ('wal_level', 'max_wal_senders');

スタンバイWAL を受け取って同じデータを持つサーバーLv55 で覚える

\! pg_basebackup -h localhost -U replicator -D /var/lib/pgsql/18/standby -R -X stream
\! pg_ctl -D /var/lib/pgsql/18/standby -o "-p 5433" start
\! psql -p 5433 -c "SELECT …"

pg_basebackup -R で写すと、standby.signal(スタンバイとして起動する印)と、postgresql.auto.conf の primary_conninfo(どこから WAL を受け取るか)ができます。起動すると、プライマリから WAL を受け取って流し続けます。スタンバイでは読むことはできますが、書くと cannot execute … in a read-only transaction になります。このシミュレーターでは、スタンバイは同じマシンの別のポートで動かし、psql -p で問い合わせます。

\! psql -p 5433 -c "SELECT pg_is_in_recovery()"

pg_stat_replicationスタンバイの様子と遅れを見るLv56 で覚える

SELECT application_name, state, sent_lsn, replay_lsn, replay_lag, sync_state FROM pg_stat_replication;
SELECT pg_wal_lsn_diff(sent_lsn, replay_lsn) FROM pg_stat_replication;
\! psql -p 5433 -c "SELECT status FROM pg_stat_wal_receiver"

プライマリの pg_stat_replication は、つながっているスタンバイごとに 1 行です。sent_lsn は送った位置、replay_lsn はスタンバイが流し終えた位置で、その差(pg_wal_lsn_diff)が遅れです。スタンバイの側では pg_stat_wal_receiver で受け取りの様子を、pg_wal_replay_pause()・pg_wal_replay_resume() で流すのを止めたり再開したりできます。

SELECT application_name, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes FROM pg_stat_replication;

レプリケーションスロットスタンバイが要る WAL を消さずに残すLv57 で覚える

SELECT * FROM pg_create_physical_replication_slot('名前');
primary_slot_name = '名前'(スタンバイの設定)
SELECT slot_name, active, restart_lsn, wal_status FROM pg_replication_slots;
SELECT pg_drop_replication_slot('名前');

スロットを使うと、スタンバイがまだ受け取っていない WAL を、プライマリが消さずに残します。止まったスタンバイがあとで追いつけます。その代わり、使われないスロット(active が f)があると WAL がたまり続け、ディスクがあふれます。要らないスロットは消し、max_slot_wal_keep_size で残す量の上限を決めておきます(超えると wal_status が lost)。

SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) FROM pg_replication_slots;

同期レプリケーションスタンバイに届いてからコミットを終えるLv58 で覚える

ALTER SYSTEM SET synchronous_standby_names = '名前';
ALTER SYSTEM SET synchronous_standby_names = 'ANY 1 (a, b)';
SET synchronous_commit = local;

synchronous_standby_names に書いたスタンバイ(primary_conninfo の application_name)に WAL が届くまで、COMMIT は終わりません。プライマリが壊れても、コミットしたデータはスタンバイにあります。その代わり、同期のスタンバイが止まると、書き込みが待ったまま止まります(Ctrl+C で待つのをやめても、ローカルではコミット済み)。ANY 1 (a, b) なら、どちらか 1 台に届けばよくなります。

SELECT application_name, sync_state FROM pg_stat_replication;

昇格(promote)スタンバイをプライマリにするLv59 で覚える

\! pg_ctl -D /var/lib/pgsql/18/standby promote
\! psql -p 5433 -c "SELECT pg_promote()"

プライマリが壊れたら、スタンバイを昇格させます。受け取った WAL を流し終えてから、新しいタイムラインで書けるようになります(0000000N.history ができる)。アプリの接続先を新しいプライマリに替えます。古いプライマリをそのまま起動すると、プライマリが 2 つになるので気をつけます。

\! pg_ctl -D /var/lib/pgsql/18/standby promote

pg_rewind分かれた古いプライマリを、新しいプライマリに合わせるLv60 で覚える

\! pg_rewind --target-pgdata=$PGDATA --source-server="host=localhost port=5433 user=postgres dbname=quest" -R

昇格のあと、古いプライマリには、スタンバイに届かなかった変更が残っていることがあります(歴史が分かれた)。pg_rewind は、分かれた位置から先を新しいプライマリに合わせて、スタンバイとしてつなげるようにします。古いプライマリは止めてから使います。設定ファイルも相手から写るので、ポートや primary_conninfo を確かめます。-R で standby.signal と primary_conninfo を書きます。

\! pg_rewind --target-pgdata=$PGDATA --source-server="host=localhost port=5433 user=postgres dbname=quest" -R

論理レプリケーション表ごとに、変更を別のサーバーへ写すLv61 で覚える

ALTER SYSTEM SET wal_level = logical;(再起動が要る)
CREATE PUBLICATION 名前 FOR TABLE 表;
CREATE SUBSCRIPTION 名前 CONNECTION 'host=… dbname=… user=… password=…' PUBLICATION 名前;(受け取る側)
ALTER SUBSCRIPTION 名前 REFRESH PUBLICATION;
SELECT * FROM pg_stat_subscription;

発行する側で PUBLICATION を作り、受け取る側で SUBSCRIPTION を作ると、まず表の中身を写し、そのあとは INSERT・UPDATE・DELETE を行ごとに送ります。受け取る側には同じ名前の表を先に作っておきます(定義は写りません)。UPDATE・DELETE を送るには主キー(REPLICA IDENTITY)が要ります。受け取る側で行がぶつかると、エラーで止まり、ログに出ます。

CREATE PUBLICATION shop_pub FOR TABLE orders;
\! psql -p 5434 -c "SELECT subname, received_lsn FROM pg_stat_subscription"