<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>VACUUM &#8211; SRA OSS Tech Blog</title>
	<atom:link href="https://www.sraoss.co.jp/tech-blog/tag/vacuum/feed/index.xml" rel="self" type="application/rss+xml" />
	<link>https://www.sraoss.co.jp/tech-blog/</link>
	<description>OSS専門の技術ブログ。PostgreSQL、Zabbixを中心に10種類以上のOSSの解説、アップデート情報、運用ヒントを提供。</description>
	<lastBuildDate>Fri, 30 Oct 2026 08:42:53 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/07/blog-icon-150x150.png</url>
	<title>VACUUM &#8211; SRA OSS Tech Blog</title>
	<link>https://www.sraoss.co.jp/tech-blog/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>PostgreSQL 性能問題の調査に役立つパラメータ設定と拡張</title>
		<link>https://www.sraoss.co.jp/tech-blog/pgsql/perf-troubleshoot-params-extension/</link>
		
		<dc:creator><![CDATA[北山 貴広 (Takahiro Kitayama)]]></dc:creator>
		<pubDate>Fri, 30 Oct 2026 07:04:01 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[ANALYZE]]></category>
		<category><![CDATA[pg_hint_plan]]></category>
		<category><![CDATA[pg_stat_statements]]></category>
		<category><![CDATA[pg_statsinfo]]></category>
		<category><![CDATA[PostgreSQL 18.6]]></category>
		<category><![CDATA[VACUUM]]></category>
		<guid isPermaLink="false">https://www.sraoss.co.jp/tech-blog/?p=21318</guid>

					<description><![CDATA[PostgreSQLサポートでは「SELECTが突然遅くなった」「データベースが全体的に遅い時間帯がある」「バッチの実行時間が5分から2時間になった」などの問い合わせをいただきます。 その際、調査をするための情報として、 ...]]></description>
										<content:encoded><![CDATA[<p>PostgreSQLサポートでは「SELECTが突然遅くなった」「データベースが全体的に遅い時間帯がある」「バッチの実行時間が5分から2時間になった」などの問い合わせをいただきます。<br />
その際、調査をするための情報として、現在の設定やログ、SQLが遅延した時に実行時間とSQL文をログに出すとか実行計画もあわせて出すとかをしておけば、遅延が発生した時や通常とは異なる状況の時に、通常とは何が違うのか、遅延時の実行計画がどうだったのかなどを調べることができます。</p>
<p>そこで、本記事では性能問題発生時の調査に必要な情報を取得するための設定や、性能遅延時の分析に有用な拡張を紹介します。</p>
<p><span id="more-21318"></span></p>
<p>パラメータ設定の対象メジャーバージョンはPostgreSQL 10以上としています。なお、検証例で使用しているPostgreSQLは PostgreSQL 18.6、OSはRocky Linux 9.5です。</p>
<h3>OSについて</h3>
<p>現在、PostgreSQLを使う時のOSの選択肢としてよくLinuxにするかWindowsにするかの選択があります。<br />
新規構築でOS選択の自由度がある場合は、Linuxを推奨します。</p>
<p>主な理由は下記の三つです。</p>
<ul>
<li>同一スペックのサーバであればLinuxのほうが性能がよい</li>
<li>Linuxでは利用可能な拡張や周辺ツールの選択肢がWindowsより豊富である(今回紹介するpg_statsinfoもWindowsでは非対応)</li>
<li>トラブル発生時の調査がLinuxの方が容易/Windowsでは困難なことが多い</li>
</ul>
<p>Linuxでは性能面・運用面のメリットが多く、利用可能な拡張も豊富であるため、トラブル調査も進めやすくなります。</p>
<h2>PostgreSQLログ関連パラメータ</h2>
<p>PostgreSQLログに関してお勧めのパラメータ設定を紹介します。</p>
<h3>ログ出力先</h3>
<p>ログの出力先(<code>log_destination</code>)は、stderrがお勧めです。syslog を使う必要がある場合でも、stderrも含めて&#8217;stderr, syslog&#8217;に設定しましょう。syslog のみ設定するのは絶対に避けましょう。<br />
stderrを含める時は、<code>logging_collector</code>もonにしておく必要があります。</p>
<pre>log_destination = 'stderr'
logging_collector = on
</pre>
<p>stderr をお勧めする理由は、<code>archive_command</code>パラメータで設定したcpコマンドが実行される際の標準エラー出力もPostgreSQLログに含まれるからです。例えばコピー先のディスクの空き容量が無くcpコマンドが失敗した時のエラーメッセージが該当します。syslog の場合は、このメッセージはsyslogに出力されずに、<code>log_destination</code>で設定されたファイルに出力されてしまいます。</p>
<p>また、syslogでは高負荷時などにPostgreSQLからのメッセージを取りこぼす可能性があるのもsyslogを避ける理由です。PostgreSQLマニュアルにも「syslogは、もし書き込みができなかったときメッセージの廃棄を選びます。」との記述があります。またsyslog側の機能としてレート制限があり、制限値以上のメッセージが到着した場合に<code>"NNN messages lost due to rate-limiting"</code>というメッセージでNNN行失われたことが伝えられます。高負荷時にこそPostgreSQLが出力するログを確認したいのに、syslogではメッセージが大量に出力される高負荷時にsyslog側でメッセージが省略されることが多々ありますので、syslogを使う必要がある場合は必ずstderrでもログを取得するようにしましょう。</p>
<h3>チェックポイントの実行と自動VACUUM/ANALYZEの実行</h3>
<p>PostgreSQLなどのデータベースでは一定間隔ごとにチェックポイントが実行されます。チェックポイントは<code>shared_buffers</code>で設定された共有バッファ上で変更されたがファイルシステム上のデータベースファイルにはまだ書き込まれていない、いわゆるダーティーバッファをすべてデータベースファイルに書き出すことで、チェックポイント終了時点のすべてのデータベースファイルで一貫性が取れている状態にすることが目的の処理です。<br />
ダーティーバッファをすべてファイルに書き出す処理ですので、Disk I/Oを使用します。SQL実行などのクライアント側の処理への影響は程度の差はあれあります。SQL実行で何らかの影響を受けたかどうかの調査で、そのタイミングでチェックポイントが実行されていたかどうかを確認するために、チェックポイント実行(<code>log_checkpoints</code>)をPostgreSQLログに出力することは有用です。</p>
<pre>log_checkpoints = on
# PostgreSQL 14までデフォルトoff、PostgreSQL 15からデフォルトon
</pre>
<p>自動VACUUMや自動ANALYZEの実行もチェックポイントと同様に、Disk I/Oを使用します。PostgreSQLログに出力する目的はチェックポイントと同様に、SQL実行時に自動VACUUMや自動ANALYZEが実行されていたかどうかを調査する観点もありますが、もう一つの観点としてSQLの実行計画が突然変わった時に、その直前で実行計画の策定に影響するオプティマイザ統計情報を更新する自動ANALYZEが実行されていたかどうかを確認する観点もあります。</p>
<pre>log_autovacuum_min_duration = 0
# PostgreSQL 14までデフォルト-1(無効)、PostgreSQL 15からデフォルト10min(10分)
</pre>
<h3>ロック待ち</h3>
<p>突然SQLの実行が遅くなった時に、ロック待ちをしていたかどうかを確認したい場合があります。デフォルトではoff(無効)となっている<code>log_lock_waits</code>をon(有効)にしておくと、<code>deadlock_timeout</code>で設定した時間(デフォルト1秒)ロック待ちをするとPostgreSQLログに出力されます。ただし<code>deadlock_timeout</code>の設定値が少なすぎるとマニュアルにも説明があるようにチェックの負荷がかかるため、本当にデッドロックが発生した場合に検知が遅くなるデメリットはありますが、チェックの負荷が軽減されるメリットもあるのでデフォルト(1秒)よりも長い時間、例えば5秒とか10秒とかを設定しましょう。下記例では10秒を設定しています。</p>
<pre>log_lock_waits = on # デフォルト off
deadlock_timeout = 10s # デフォルト 1s
</pre>
<h3>遅延SQL</h3>
<p><code>log_min_duration_statement</code>(デフォルト:-1(無効))に設定した時間以上実行にかかったSQLはPostgreSQLログに出力されます。<br />
下記設定例では、10秒を設定していますので、10秒以上かかったSQLはPostgreSQLログに出力されます。</p>
<pre>log_min_duration_statement = 10s # デフォルト-1(無効)
</pre>
<h3>上記の設定をすべて実行した場合</h3>
<p>上記の設定をすべて実行した場合、SQLの遅延が発生するとSQL文が実行時間と共にPostgreSQLログに出力されます。例えばSQL Aが11秒かかったことが分かり、その際、一緒にロック待ちのログが出ていれば、ロック待ちの影響で遅延が発生したことがわかります。<br />
また、遅延が発生したタイミングで自動VACUUMもしくはチェックポイントが実行されていたとか、遅延が発生する直前に自動ANALYZEが実行されていたかもわかりますので、同じタイミングで実行されていた自動VACUUMやチェックポイントの可能性があるかないか、また自動ANALYZEにより実行計画を作成する元となるオプティマイザ統計情報が変更されて実行計画が変更された可能性があるかないかが判断可能となります。<br />
しかし、SQLが遅延した場合に実際に実行計画が変化したのかどうかまではわかりません。SQLが遅延した時に実行計画の情報をPostgreSQLログに出力するauto_explainモジュールについて後で紹介します。</p>
<h2>メモリ設定関連パラメータ</h2>
<h3>共有バッファ</h3>
<p>PostgreSQLマニュアルには1GB以上物理メモリがあるサーバでの<code>shared_buffers</code>の妥当な初期値は物理メモリの25%という説明が記載されていますので、共有バッファはサーバの物理メモリの4分の1に設定しましょう。例えば、物理メモリが64GBならば16GBを設定します。下記設定例は、物理メモリが64GBの場合です。</p>
<pre>shared_buffers = 16GB
</pre>
<p>サーバのメモリがどのように使用されるか、使用される比率について説明します。<br />
PostgreSQLはOSのファイルシステム上にデータベースのファイルを作成しますので、PostgreSQLの共有メモリで全体の25%、OS側でファイルのキャッシュとして全体の25%、合わせると物理メモリ全体の半分(50%)がPostgreSQLのデータベースファイルで使われ、残りの半分(50%)でOS自体が使用するメモリとOSが起動するプロセス、そしてPostgreSQLのプロセスがメモリを使用します。</p>
<h3>ワークメモリ</h3>
<p>SQLを実行する際にソートやハッシュ検索などの処理ではPostgreSQLプロセスが<code>work_mem</code>で設定された値を上限としてメモリを使用しようとしますが、設定された値では足りない場合にディスクを使って読み書きをして処理をします。メモリ上の読み書きに比べてディスク上の読み書きは桁違いに遅いので、SQLの実行に影響しますので、サーバのメモリが不足しない程度に<code>work_mem</code>に大きな値を設定します。下記の例では、16MBを設定しています。</p>
<pre>work_mem = 16MB
</pre>
<p><code>work_mem</code>で設定した値でメモリ上で処理が済んでいるかどうかを確認するには、ディスク上の処理が発生した場合にPostgreSQLログに出力する<code>log_temp_files</code>を0に設定して、SQLを実行します。ディスクを使用したら、ファイル削除時にPostgreSQLログに使用した一時ファイルが出力されます。</p>
<pre>log_temp_files = 0 # デフォルト -1(無効)
</pre>
<p>postgresql.confで上記の設定をすると、ディスク上の処理がすべてPostgreSQLログに出てしまいますので、一時的に<code>log_temp_files=0</code>に変更して<code>pg_ctl reload</code>で設定を反映、ディスク上の使用有無をチェックするSQL文を実行、<code>log_temp_files=-1</code>に戻して<code>pg_ctl reload</code>で設定を反映することで、調査期間の間だけPostgreSQLログに出力することが可能です。</p>
<p>また、特定のSQLを取り出してpsqlで実行可能な場合は、下記の例のようにセッション単位で<code>log_temp_files=0</code>に設定してSQLを実行することも可能です。psqlの画面上に表示するために<code>client_min_messages</code>も設定することで、<code>LOG: temporary file: path "base/pgsql_tmp/pgsql_tmp13483.0", size 10731520</code>が表示されて一時ファイルが使用されていることが分かります。</p>
<pre>db1=# SET log_temp_files = 0;
SET
db1=# SHOW log_temp_files;
log_temp_files
----------------
0
(1 row)

db1=# SET client_min_messages = log;
SET
db1=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts ORDER BY abalance DESC;
LOG: temporary file: path "base/pgsql_tmp/pgsql_tmp13483.0", size 10731520
QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------
----
Sort (cost=16476.32..16726.32 rows=100000 width=97) (actual time=65.871..80.323 rows=100000.00 loops=1)
Sort Key: abalance DESC
Sort Method: external merge Disk: 10480kB
Buffers: shared hit=1704, temp read=1310 written=1313
-&gt; Seq Scan on pgbench_accounts (cost=0.00..2701.00 rows=100000 width=97) (actual time=0.022..14.887 rows=100000.00 loops
=1)
Buffers: shared hit=1701
Planning:
Buffers: shared hit=73
Planning Time: 0.571 ms
Execution Time: 89.080 ms
(10 rows)
</pre>
<p>上記のSQLで一時ファイルのDisk I/Oを使用しない<code>work_mem</code>の値を調べるには、実行計画でDiskが出なくなる<code>work_mem</code>の値を、セッション単位で一時的に必要最小限の大きな値に設定して、実行計画を表示させます。下記例では、16MBに設定した時に、<code>external merge Disk</code>が<code>quicksort Memory</code>に変わりましたので、このSQLの実行直前にセッション単位のSET文で<code>SET work_mem ='16MB';</code>に設定すればメモリ上のソートで性能向上が見込めます。</p>
<pre>db1=# SHOW work_mem;
work_mem
----------
4MB
(1 row)

db1=# SET work_mem = '16MB';
SET
db1=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts ORDER BY abalance DESC;
QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------
----
Sort (cost=11005.82..11255.82 rows=100000 width=97) (actual time=38.479..51.692 rows=100000.00 loops=1)
Sort Key: abalance DESC
Sort Method: quicksort Memory: 14791kB
Buffers: shared hit=1701
-&gt; Seq Scan on pgbench_accounts (cost=0.00..2701.00 rows=100000 width=97) (actual time=0.020..12.699 rows=100000.00 loops
=1)
Buffers: shared hit=1701
Planning Time: 0.084 ms
Execution Time: 56.287 ms
(8 rows)

</pre>
<h2>SQL遅延時の対応について</h2>
<p>auto_explain拡張を紹介する前に、SQLが遅延した時にどのような対応を取るのかについて説明します。</p>
<h3>オプティマイザ統計情報の最新化と不要領域の整理</h3>
<p>SQLの遅延が発生した時にまず実行してほしいのが、SQLの実行計画の基となっているオプティマイザ統計情報の最新化と、SQLの検索対象テーブルの不要領域の整理です。<br />
SQLの検索対象となっているテーブル全て、特に対象テーブルのみに限定する必要が無い場合はデータベースに対して<code>VACUUM (ANALYZE)</code>を実行します。</p>
<p>下記実行例では、どのような実行状況なのかを表示するために<code>VERBOSE</code>オプションを付けています。</p>
<pre>VACUUM (ANALYZE,VERBOSE) 検索対象テーブル;
</pre>
<p>もしくは、対象データベースに接続した状態で、引数を省略するとデータベース全体に対して<code>VACUUM (ANALYZE)</code>が実行されます。</p>
<pre>VACUUM (ANALYZE,VERBOSE);
</pre>
<p>上記コマンドの実行で、現在テーブルに入っているデータで実行計画を作成するベースとなるオプティマイザ統計情報を最新化するとともに、削除済みだがまだ回収されていない不要領域が回収されます。そのため、実行計画を策定するためのオプティマイザ統計情報がテーブルの最新状況を反映していない乖離した古い状態で、効率の悪い不適切な実行計画によりSQLが遅延していた場合は、統計情報が最新化された後の再実行では遅延が解消されます。</p>
<h3>実行計画の取得</h3>
<p>上記の<code>VACUUM (ANALYZE)</code>をSQLの対象テーブルすべてに対して実行しても性能が改善しない場合、例えば普段0.5秒程度で実行されているSQLが急に10秒とかかかるようになった時に取得が必要な情報が<code>EXPLAIN (ANALYZE, BUFFERS) 遅延SQL;</code>で出力される実行計画です。</p>
<pre>EXPLAIN (ANALYZE, BUFFERS) 遅延SQL;
</pre>
<p><code>EXPLAIN</code>コマンドは引数のSQLをどのように実行するかの詳細ステップを書いた実行計画を出力するコマンドで、<code>ANALYZE</code>オプションは実際に実行した時間を出力、<code>BUFFERS</code>オプションはファイルアクセス時に共有バッファから読み込んだのかファイルから読み込んだのかの情報とアクセスしたバッファ数を出力します。</p>
<p>実行例を下記に示します。1回目はDB起動直後、2回目は連続して実行しました。2回の違いとして、<code>shared read</code>(Diskからの読み込み)が<code>shared hit</code>(共有バッファからの読み込み)になっていること、一度読み込まれて共有バッファからの読み込みになったので2回目の時間が<code>Planning</code>も<code>Execution</code>も短くなっていることが分かります。</p>
<pre>$ psql db1
psql (18.6)
Type "help" for help.

db1=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Seq Scan on pgbench_accounts (cost=0.00..2640.00 rows=100000 width=97) (actual time=2.176..28.884 rows=100000.00 loops=1)
Buffers: shared read=1640
Planning:
Buffers: shared hit=44 read=11
Planning Time: 0.846 ms
Execution Time: 33.923 ms
(6 rows)

db1=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Seq Scan on pgbench_accounts (cost=0.00..2640.00 rows=100000 width=97) (actual time=0.069..9.271 rows=100000.00 loops=1)
Buffers: shared hit=1640
Planning Time: 0.070 ms
Execution Time: 17.789 ms
(4 rows)

db1=#
</pre>
<p>上記の実行結果を、通常の速い時に取っておき、遅延が発生した遅い時にも取れば、実行計画そのものが変わったのか実行計画は同一なのか、遅い時はどのステップでどの程度遅くなっているのかの比較ができて、通常の速い時とどう違って遅いのかの理由が分かります。</p>
<p>なお、どの実行計画のどの段階でどの列の結果を出力しているのかのオプションである<code>VERBOSE</code>オプションもあわせて指定すると、実行計画がどのように動作しているのか分かりやすくなります。下記実行例では、Outputの行が<code>VERBOSE</code>オプションによって追加された情報になります。</p>
<pre>db1=# EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM pgbench_accounts;
QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------
-----
Seq Scan on public.pgbench_accounts (cost=0.00..2640.00 rows=100000 width=97) (actual time=0.018..11.627 rows=100000.00 loop
s=1)
Output: aid, bid, abalance, filler
Buffers: shared hit=1640
Planning:
Buffers: shared hit=60
Planning Time: 1.456 ms
Execution Time: 19.776 ms
(7 rows)

db1=#
</pre>
<h2>拡張(EXTENSION)</h2>
<p>PostgreSQLの⼤きな特徴として、様々な機能を追加できる拡張(EXTENSION)があります。<br />
ここでは性能問題の調査や分析に役立つ代表的な拡張を紹介します。</p>
<p>拡張を使用するためには、拡張が入ったパッケージをPostgreSQLインストール時にあらかじめインストールしておき、<code>CREATE EXTENSION</code>や<code>shared_preload_libraries</code>変更などの設定が必要ですが、そもそも拡張をインストールしていない場合は、拡張を使用する設定をする前に後から追加でインストールが必要となります。<br />
しかし、既にシステムが本番稼働をした状況で、後から拡張をインストールをするのはハードルが高いので、後から使いたい時に使えるよう、PostgreSQLインストール時に一緒にインストールしておくのが望ましいです。</p>
<h3>拡張の種類</h3>
<p>PostgreSQL本体と一緒に同梱されているcontribモジュールと、contribモジュール以外(サードパーティー)モジュールがあります。</p>
<p>contribモジュールは、PostgreSQL本体と合わせてリリースされていますので、PostgreSQLサーバのインストール時に一緒にインストールしておきましょう。contribモジュールをインストールしておけば、後からcontribモジュールに含まれる拡張を導入することが可能です。contribモジュール以外の拡張は、サードパーティーモジュールと呼ばれ、現在はGitHubサイトで公開されていることが多いです。ダウンロード可能な形式はソースコードがメインで、リリース時点で公開されているRHELのrpmファイルもダウンロード可能な場合もあります。</p>
<h3>auto_explain(contrib)</h3>
<p>設定した時間以上かかったSQLの実行計画をPostgreSQLログに出力します。今回のこの記事で一番紹介したかったのはこのauto_explain拡張モジュールです。</p>
<p>サポートでよく「SQLが突然遅くなった。」という問い合わせをいただきますが、遅くなった時の情報が何もないため、なぜ遅くなったのかの原因調査が出来ません。問い合わせをいただいた時の返答はいつも「次回遅延が発生している間に<code>EXPLAIN (ANALYZE, BUFFERS) SQL;</code>を実行して実行計画を取得してください。」とお伝えしています。遅延している状況がずっと続いている、または定期的に遅延していてその時間帯が日中の場合は<code>EXPLAIN (ANALYZE, BUFFERS) SQL;</code>を手動で実行して実行結果を取得することは可能ですが、遅延の発生が真夜中であるとか、遅延がいつ発生するのかが不明な場合は人手で<code>EXPLAIN (ANALYZE, BUFFERS) SQL;</code>を実行することは困難です。そのような状況で一番役立つのがこのauto_explainです。</p>
<p>auto_explainの使い方について説明します。postgresql.confに下記の設定を追加して、PostgreSQLを再起動するとauto_explainが有効になって、例えば10秒以上かかるSQLが実行されると自動的にSQL実行時の<code>EXPLAIN(ANALYZE, BUFFERS) SQL;</code>の情報がPostgreSQLログに出力されます。</p>
<p>PostgreSQLログにauto_explainでの実行結果が出力されれば、性能悪化時と通常時の実行計画を比較できます。<br />
下記はauto_explainを使うために必要な設定で、設定追加後PostgreSQLを再起動すると10秒以上かかったSQL文の実行計画がPostgreSQLログに出力されます。</p>
<pre>shared_preload_libraries = 'auto_explain'
auto_explain.log_min_duration = 10s
auto_explain.log_analyze = on
auto_explain.log_buffers = on
auto_explain.log_timing = off # ノードごとの実行時間を取得する場合はon
</pre>
<p>なお、auto_explainの有効化はデータベースサーバ全体だけではなく、セッション単位でも下記の方法で有効化可能です。下記の例は、psqlで接続した後、</p>
<pre>db1=# LOAD 'auto_explain';
LOAD
db1=# set auto_explain.log_min_duration = 0;
SET
db1=# set auto_explain.log_analyze = on;
SET
db1=# SELECT COUNT(*) FROM pgbench_accounts;
count
--------
100000
(1 row)

db1=#
</pre>
<p>上記の実行でのPostgreSQLログ出力は下記となります。</p>
<pre>2026-10-06 20:51:58.504 JST [5786] LOG: duration: 7.090 ms plan:
Query Text: SELECT COUNT(*) FROM pgbench_accounts;
Aggregate (cost=2924.00..2924.01 rows=1 width=8) (actual time=7.080..7.081 rows=1.00 loops=1)
-&gt; Seq Scan on pgbench_accounts (cost=0.00..2674.00 rows=100000 width=0) (actual time=0.008..4.326 rows=100000.00 loops=1)
</pre>
<p>※注：<code>auto_explain.log_analyze</code>を有効(on)にすると、すべてのSQL実行で性能上のマイナスが起こり得ます。<code>auto_explain.log_timing = off</code>を追加することでノードごとの時間情報が無効になりますが行数の情報は取得でき性能面での損失が改善されますので、本番環境では通常は<code>auto_explain.log_timing</code>を無効にしておくことが望ましいと考えられます。<br />
実際に遅延が発生し、ノードごとの時間情報も必要と判断した場合のみ、性能面でのデメリットを認識した上で<code>auto_explain.log_timing = on</code>に変更し、情報が取得でき次第速やかに無効(<code>off</code>)にする運用がよいでしょう。</p>
<p>auto_explain(F.3. auto_explain — 低速な問い合わせ実行計画のログ)<br />
<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/auto-explain.html" target="_blank" rel="noopener">https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/auto-explain.html</a></p>
<h4>pg_hint_plan(サードパーティー)</h4>
<p>なお、今回紹介している性能調査とは異なりますが、auto_explainでPostgreSQLログに出力した実行計画が性能悪化の原因であった場合、SQLに埋め込んだヒント句で強制的に実行計画を変更するpg_hint_planという拡張も紹介しておきます。<br />
オプティマイザ統計情報を最新化しても実行計画の形が変わってしまってSQLの性能が悪化した場合、例えば結合方式が速い時は<code>Hash Join</code>なのに遅延時は<code>Nested Loop</code>に変更されてしまったとか、複数テーブルある結合で速い時と遅い時でテーブルを結合する順番が異なるなどの場合に、強制的に速い時の実行計画にあわせるためにSQLにヒント句を埋め込んでヒント句の通りに実行計画を強制変更します。SQLにヒント句を埋め込まない指定も利用可能です。弊社ブログに解説がありますのでご参照ください。</p>
<p>pg_hint_plan (PostgreSQL 実行計画制御ツール)<br />
<a href="https://www.sraoss.co.jp/tech-blog/pgsql/pg_hint_plan" target="_blank" rel="noopener">https://www.sraoss.co.jp/tech-blog/pgsql/pg_hint_plan/</a></p>
<h3>pg_stat_statements(contrib)</h3>
<p>pg_stat_statementsは日常的にどのSQLが時間を使っているのかを把握するための拡張です。短い実行時間のSQLでも実行回数が多ければ合計実行時間は大きくなります。pg_stat_statementsはサーバで実行されたすべてのSQLのプラン生成時と実行時の統計情報を記録します。このモジュールはPostgreSQLログへの出力は無く、情報の参照は情報を取得しているデータベースに接続してpg_stat_statementsビューをSELECTする方法となります。</p>
<p>下記はpg_stat_statementsを使うために必要な設定で、設定追加後PostgreSQLを再起動した後、利用するデータベースに接続して<code>CREATE EXTENSION pg_stat_statements;</code>を実行すると、pg_stat_statementsビューが参照可能となります。</p>
<pre>shared_preload_libraries = 'pg_stat_statements'
</pre>
<p>DB再起動後、下記を実行。</p>
<pre>db1=# CREATE EXTENSION pg_stat_statements;
CREATE EXTENSION
db1=# \dx
List of installed extensions
Name | Version | Default version | Schema | Description

--------------------+---------+-----------------+------------+----------------------------------------------------------------
--------
pg_stat_statements | 1.12 | 1.12 | public | track planning and execution statistics of all SQL statements e
xecuted
plpgsql | 1.0 | 1.0 | pg_catalog | PL/pgSQL procedural language
(2 rows)

db1=#
</pre>
<p>PostgreSQLマニュアルの実行例を実行した例。合計実行時間(<code>total_exec_time</code>)の多い順から、SQL、呼び出し回数、合計実行時間、行数、算出したヒット率が表示されています。</p>
<pre>db1=# \x
Expanded display is on.
db1=# SELECT query, calls, total_exec_time, rows, 100.0 * shared_blks_hit /
nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;
-[ RECORD 1 ]---+-----------------------------------------------------------------------------------------------------
query | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2
calls | 3000
total_exec_time | 17180.62709200005
rows | 3000
hit_percent | 99.9982951155059245
-[ RECORD 2 ]---+-----------------------------------------------------------------------------------------------------
query | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2
calls | 3000
total_exec_time | 14783.155679999958
rows | 3000
hit_percent | 99.9957805907172996
-[ RECORD 3 ]---+-----------------------------------------------------------------------------------------------------
query | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2
calls | 3000
total_exec_time | 111.09148400000022
rows | 3000
hit_percent | 92.5940860215053763
-[ RECORD 4 ]---+-----------------------------------------------------------------------------------------------------
query | SELECT abalance FROM pgbench_accounts WHERE aid = $1
calls | 3000
total_exec_time | 28.88099300000003
rows | 3000
hit_percent | 100.0000000000000000
-[ RECORD 5 ]---+-----------------------------------------------------------------------------------------------------
query | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
calls | 3000
total_exec_time | 21.792536000000027
rows | 3000
hit_percent | 100.0000000000000000

db1=#
</pre>
<p>pg_stat_statements(F.32. pg_stat_statements — SQL文のプラン生成時と実行時の統計情報を記録する)<br />
<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/pgstatstatements.html" target="_blank" rel="noopener">https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/pgstatstatements.html</a></p>
<h3>性能問題の調査に役立つ情報の継続収集</h3>
<p>性能問題の調査では、問題発生時の状況だけでなく、過去との比較も重要です。<br />
そのためには PostgreSQL の統計情報や OS のリソース情報を継続的に収集しておくことが有効です。</p>
<p>収集方法としては、</p>
<ul>
<li>sysstat(sar)</li>
<li>各種監視ツール</li>
<li>PostgreSQL向け収集ツール</li>
</ul>
<p>などがあります。<br />
その中でも PostgreSQL の統計情報と OS リソース情報をまとめて時系列で取得できるツールとしてpg_statsinfo があります。</p>
<h4>pg_statsinfo(サードパーティー)</h4>
<p>pg_statsinfo は PostgreSQL の統計情報やOSリソース情報を時系列で取得・保存するツールです。PostgreSQL 統計情報やOSリソース情報を時系列で取得するスナップショットを保存するためのリポジトリDBが必要となりますが、pg_statsinfoを使った運用をしていると何か性能問題が発生した時に過去の情報を参照できるのでとても便利です。上で紹介したpg_stat_statementsと、ヒント句を取得できるpg_store_plansと併用することでクエリの統計情報と実行計画も一緒に保存できるため、どのSQLが性能に影響していた可能性があるのか、また実行計画の変化によって性能が悪化した時も後から確認できます。<br />
弊社ブログに解説がありますのでご参照ください。</p>
<p>pg_statsinfo(統計・OSリソース情報時系列取得保存ツール)<br />
<a href="https://www.sraoss.co.jp/tech-blog/pgsql/pg_statsinfo/" target="_blank" rel="noopener">https://www.sraoss.co.jp/tech-blog/pgsql/pg_statsinfo/</a></p>
<p>pg_statsinfoは現在も有用なツールですが、監視製品やクラウド環境の監視機能などを利用して継続収集を行う方法も合わせてご検討ください。</p>
<h2>まとめ</h2>
<p>PostgreSQLの性能問題は、「遅くなった」という事実だけでは原因を特定できません。重要なのは、問題発生時に何が起きていたのかを後から分析できる情報を残しておくことです。</p>
<p>本記事では、チェックポイント、自動VACUUM、自動ANALYZE、ロック待ち、遅延SQLを記録するためのログ設定と、遅延時の実行計画を自動取得できるauto_explain、調査結果を踏まえて実行計画を制御できるpg_hint_plan、SQL実行統計を蓄積するpg_stat_statements、PostgreSQLの統計情報やOSリソース情報を時系列で取得・保存するpg_statsinfoを紹介しました。</p>
<p>特に、SQL性能問題の原因究明においては、遅延発生時のEXPLAIN (ANALYZE, BUFFERS)の取得が最も重要です。auto_explainを導入しておけば、人が操作できない時間帯に発生した遅延であっても、実行計画を記録できます。遅延時の実行計画が通常時の実行計画と異なる場合は、pg_hint_planにより強制的に通常時の実行計画に合わせることが可能となります。ただし、実行計画変更の根本原因が統計情報やデータ分布の変化にある場合もあり、pg_hint_planは原因調査や恒久対策を行った上で必要に応じて利用します。</p>
<p>PostgreSQLの性能問題への対応は、問題発生後に慌てて調査方法を考えるのではなく、平常時から必要な情報を取得できる状態を作っておくことが重要です。本記事がそのための設定見直しの参考になれば幸いです。</p>
<p>&nbsp;</p>
<div>
<a href="https://www.sraoss.co.jp/prod_serv/support/pgsql-mainte/?utm_source=sraoss-techblog&utm_medium=referral&utm_campaign=under-article-pgsql" rel="noopener" style="color:initial;" target="_blank" class="no-ext"></p>
<div style="background-color:#DAEFF6; padding:20px; border-radius:8px; text-align:center;">
SRA OSSでは、PostgreSQL/Pgpool-II のサポートを提供しております。<br />
詳しくは<strong style="color:#2581c4;">弊社ホームページ</strong>をご覧ください。
</div>
<p></a>
</div>

]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>VACUUM処理が阻害されるケースとその原因・検知・対策</title>
		<link>https://www.sraoss.co.jp/tech-blog/pgsql/hinderingvacuum/</link>
		
		<dc:creator><![CDATA[北山 貴広 (Takahiro Kitayama)]]></dc:creator>
		<pubDate>Wed, 15 Apr 2026 04:35:24 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[VACUUM]]></category>
		<category><![CDATA[トランザクションID]]></category>
		<guid isPermaLink="false">https://www.sraoss.co.jp/tech-blog/?p=20380</guid>

					<description><![CDATA[この記事のポイント VACUUM コマンドや 自動VACUUM は成功するのだけれども（コマンド応答にもログにも ERROR などは出ないけれども）、実際にはデッドタプルが回収されない状況が発生することがあります。 VA ...]]></description>
										<content:encoded><![CDATA[<h2>この記事のポイント</h2>
<p>VACUUM コマンドや 自動VACUUM は成功するのだけれども（コマンド応答にもログにも ERROR などは出ないけれども）、実際にはデッドタプルが回収されない状況が発生することがあります。<br />
VACUUMが期待通りに動作しない場合、以下を確認することで原因特定が可能です。</p>
<ul>
<li>ログに「are dead but not yet removable」が出ているか(*)</li>
<li>ログの「removable cutoff (PostgreSQL 16以上) もしくは oldest xmin (PostgreSQL 15以下)」の数字が進んでいないか(*)</li>
<li>以下のビューを確認する(ストリーミングレプリケーション環境の場合はスタンバイ側も)
<ul>
<li>pg_stat_activity</li>
<li>pg_replication_slots</li>
<li>pg_prepared_xacts</li>
</ul>
</li>
</ul>
<p>主な原因は以下の3つです。</p>
<ol>
<li>ロングトランザクション</li>
<li>レプリケーションスロット</li>
<li>プリペアドトランザクション</li>
</ol>
<p>対策の本質は 「oldest xminを固定している要因を解消すること」 です。<br />
(*) log_autovacuum_min_duration パラメータを0や正の数で設定して自動VACUUM実行結果をログに出力する必要があります。</p>
<p><span id="more-20380"></span></p>
<h2>はじめに</h2>
<p>VACUUMが実行する二つの主要な処理として、削除された行(dead tuples)の領域回収と、トランザクションID(XIDと表記される)周回防止のための凍結があります。<br />
一つ目は、UPDATEやDELETEなどで削除された行(dead tuples)を回収して再利用可能な領域にする処理で、この機能が正常に働くことにより追記型のPostgreSQLで必要以上にデータベースサイズが大きくなることが防止されます。<br />
二つ目は、32bitの非負数の整数(約42億)のトランザクションID(XID)では現在のXIDから前半分が過去、後ろ半分が未来というルールで管理されています。過去の行が現在のXIDより約21億前の半分を過ぎると突然未来になって見えなくなる状況を防ぐために必要なのがトランザクションIDの凍結(FREEZE)で、凍結処理が行われた行はどのトランザクションからも見えるようになります。</p>
<p>本記事では、VACUUMによるこの二つの働きを阻害する原因、検知と対応について説明します。</p>
<h2>VACUUMによる行回収処理</h2>
<h3>PostgreSQLは「追記型」アーキテクチャ</h3>
<p>PostgreSQLデータベースの特徴として「追記型」アーキテクチャであることが挙げられます。他のデータベースではUPDATEによる行の更新方法が更新前の行を更新後の値で書き換える「書き換え型」が多く採用されていますが、PostgreSQLではUPDATE時に更新前の行のxmaxにトランザクションIDを記録して「削除された行」という印をつけておき、更新後の行を別の場所に挿入する「追記型」を採用しています。またDELETEも実行時には該当行のxmaxにトランザクションIDを記録して「削除された行」という印をつけるのみです。この方法により、削除される前から「削除された行」を参照しているトランザクションがUPDATEと並行して実行を継続することができます。</p>
<h3>VACUUMの行回収</h3>
<p>この「追記型」ゆえに発生した「削除された行(dead tuples)」の領域を回収して再利用可能な領域とするために、PostgreSQLには「VACUUM」という機能があり、あらかじめ決められた一定の条件を満たすとテーブルに対して自動的にVACUUMを実行する「自動VACUUM」がデフォルトで有効になっています。<br />
「自動VACUUM」が実行される条件が成立すると、自動VACUUMが実行されて「削除された行」が回収されて再利用可能な領域になり、次のUPDATEやINSERTなどで利用できます。この機能が有効に働くことで、PostgreSQLのファイルが無駄に肥大化し続ける状況が防げます。</p>
<h3>ロングトランザクションによる行回収処理阻害</h3>
<p>「自動VACUUM」が実行される条件が成立すると、自動VACUUMが実行されて阻害要因が無いと「削除された行」が回収されますが、「削除された行」が回収できない阻害要因が存在すると回収できない状況となります。その要因となる一番の原因が「ロングトランザクション」の存在です。「削除された行」を参照しているトランザクションが長時間実行され続ける「ロングトランザクション」が存在して回収を阻害している状況では、自動VACUUMを実行しても「削除された行」が回収できません。その状況では、自動VACUUMの実行が完了しても「削除された行」が回収できないためまた自動VACUUMが実行され、回収できない阻害要因を排除するまで無駄な自動VACUUMの実行が繰り返し行われてしまいます。この状況がずっと続くと、「トランザクションIDの周回問題」が発生してしまう深刻な状況に陥ります。</p>
<p>&nbsp;</p>
<h2>トランザクションID(XID)の周回問題</h2>
<h3>トランザクションIDの凍結処理と阻害された時の影響</h3>
<p>PostgreSQLのトランザクションID(XID)は32bitの非負数で約42億と循環する値で管理されており、基準となるXIDより古い値の21億が過去、基準点となるXIDより新しい値の21億が未来となっています。基準となるXIDより古い値の21億よりも値が古くなると(XIDが進むと)、突然未来となってしまい基準となるXIDから見ると未来の範囲に入るため、該当行が見えなくなってしまいます。そのため、XIDが21億進んでしまうまでの間に、VACUUMによる凍結(FREEZE)処理が必要となります。具体的は、行が作成された時のXIDが記録されるxminに凍結した印をつける処理をします。この処理が進まない場合、データベース内で凍結されていないXIDの下限値であるdatfrozenxidから現在のトランザクションIDまでのトランザクション数を計算するage(datfrozenxid)が増加し続けて、最悪の場合データベースが参照のみとなり更新不可の状態となります。</p>
<p>この問題の詳細な仕組みについては、弊社の下記のBlog記事が非常に分かりやすく整理されています。<br />
<a href="https://www.sraoss.co.jp/tech-blog/pgsql/transaction-id-wraparound/">（参考）トランザクションID周回問題について</a><br />
なお、トランザクションIDの凍結(FREEZE)処理も、削除された行(dead tuples)の回収と同様に、「古いxminが残り続けること」によって進まなくなります。そのため、ロングトランザクションなどの存在は、領域回収の阻害だけでなく、トランザクションIDの周回防止処理の遅延の原因となります。<br />
トランザクションIDの周回防止処理の阻害要因は、削除された行(dead tuples)の回収を阻害する要因と同じですので、以下では行回収阻害について説明します。</p>
<h2>行回収阻害発生時に出るログ</h2>
<p>実際にロングトランザクションが存在してVACUUMによる「削除された行(dead tuples)」の回収が阻害された時に出るログの該当箇所を下記に示します。</p>
<h3>最も着目すべきログの箇所</h3>
<p>PostgreSQL 15までとPostgreSQL 16からログ表記が変わってますので、下記では現時点でもっとも古いサポート対象メジャーバージョンのPostgreSQL 14.22でのログと、現時点でもっとも新しいサポート対象メジャーバージョンのPostgreSQL 18.3でのログを使って説明します。<br />
行回収が阻害されている場合、VACUUMのログには以下の特徴的なメッセージが出力されます。特に「are dead but not yet removable」は、行回収阻害を判断する最も重要な指標です。</p>
<pre>(PostgreSQL 14.22)
tuples: 0 removed, 1499989 remain, 500000 are dead but not yet removable, oldest xmin: 737
(PostgreSQL 18.3)
tuples: 0 removed, 1499989 remain, 500000 are dead but not yet removable
removable cutoff: 1124, which was 1 XIDs old when operation ended
</pre>
<h3>回収が阻害された行数は「are dead but not yet removable」の前の数字がその行数である</h3>
<p>行回収阻害を表すのは「are dead but not yet removable」の文字列、意味は「～は削除されているが回収(消去/再利用可能に)できない」で行回収阻害そのものを説明しています。この文字列の前にある数字(14.22も18.3も500000)が、削除されたが回収できなかった行数で、その原因はこれらの行をまだ参照する可能性がある古いトランザクション(ロングトランザクション)が残っているためです。</p>
<h3>古いトランザクションIDの表記: PostgreSQL 15までは「oldest xmin」PostgreSQL 16からは「removable cutoff」</h3>
<p>その古いトランザクションのトランザクションIDの表記は、PostgreSQL 15までは「are dead but not yet removable」と同じ行でこの後に「oldest xmin」が配置されていて、PostgreSQL 16からは「are dead but not yet removable」の次の行に「removable cutoff」が配置されています。<br />
PostgreSQL 16からは、その後に「which was NNN XIDs old when operation ended」と「removable cutoff」のXIDが、操作が終わってから現在のXIDとの差異のXIDとともに配置されています。<br />
PostgreSQLのソースコードでは、xmin horizonという単語も使われています。</p>
<h4>(参考) 実行ログ全体</h4>
<p>上記のメッセージを含むVACUUM実行全体のログは下記となります。PostgreSQL 18ではPostgreSQL 14に比べて出力される内容が増えていて、removable cutoff、frozen、visibility mapの行が追加されていることが分かります。</p>
<h5>PostgreSQL 14.22 でのメッセージ</h5>
<pre>2026-03-26 17:05:36.524 JST [14062] LOG: automatic vacuum of table "db1.public.vacuum_test": index scans: 0
pages: 0 removed, 14019 remain, 0 skipped due to pins, 0 skipped frozen
tuples: 0 removed, 1499989 remain, 500000 are dead but not yet removable, oldest xmin: 737
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 5.784 MB/s, avg write rate: 12.032 MB/s
buffer usage: 17334 hits, 1448 misses, 3012 dirtied
WAL usage: 1 records, 0 full page images, 188 bytes
system usage: CPU: user: 0.13 s, system: 0.02 s, elapsed: 1.95 s
</pre>
<h5>PostgreSQL 18.3 でのメッセージ</h5>
<pre>2026-03-26 17:09:16.385 JST [14250] LOG: automatic vacuum of table "db1.public.vacuum_test": index scans: 0
pages: 0 removed, 14019 remain, 9347 scanned (66.67% of total), 0 eagerly scanned
tuples: 0 removed, 1499989 remain, 500000 are dead but not yet removable
removable cutoff: 1124, which was 1 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 14.030 MB/s, avg write rate: 15.176 MB/s
buffer usage: 17137 hits, 1615 reads, 1747 dirtied
WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full
system usage: CPU: user: 0.07 s, system: 0.03 s, elapsed: 0.89 s
</pre>
<h3>自動VACUUM実行ログ出力設定</h3>
<p>上記の自動VACUUMの実行状況をPostgreSQLログに出力するには、postgresql.conf で設定した値以上の時間がかかった自動VACUUM実行をログ出力するlog_autovacuum_min_durationパラメータを設定します。PostgreSQL 14までは-1(無効、まったく出さない)、PostgreSQL 15からは10min(10分)ですが、この値を0(数字のゼロ)に設定することで、すべての自動VACUUMと自動ANALYZEの実行がPostgreSQLログに記録されますので、自動VACUUMの実行状況が把握でき、削除された行回収が阻害されている時も検知が可能となります。</p>
<pre>log_autovacuum_min_duration = 0
</pre>
<p>本記事では、上記の設定値ですべての自動VACUUM/自動ANALYZEがPostgreSQLログに出力される前提としています。<br />
ちなみに、VACUUMコマンドを実行する時もVACUUM (VERBOSE)と詳細ログを出力するようにしてください。<br />
また余談となりますが、上記の設定と下記のチェックポイント実行状況をログに出力する</p>
<pre>log_checkpoints = on
</pre>
<p>の設定(PostgreSQL 15からデフォルトon)で、すべての自動VACUUM/自動ANALYZEの実行状況と、すべてのチェックポイントの実行状況を把握できますので、これらの設定値にすることをお勧めします。</p>
<h2>行回収を阻害する「oldest xmin (PostgreSQL 15以下) / removable cutoff (PostgreSQL 16以上)」の見つけ方とその原因</h2>
<p>VACUUMによる行回収を阻害する「oldest xmin」や「removable cutoff」を、どのように見つけるのか、また何が原因なのかについて説明します。<br />
原因としては、大きく三つあります。これらの原因はいずれも「xminを古い位置に固定してしまう」という共通点があります。<br />
1. ロングトランザクション(同一サーバ上/hot_standby_feedback = on のスタンバイ)(原因1)<br />
2. 未使用のレプリケーションスロット(原因2)<br />
3. 放置されたプリペアドトランザクション(PREPARED TRANSACTIONにより作成)(原因3)</p>
<h3>ロングトランザクション(原因その1)</h3>
<p>ロングトランザクションとは「長い」トランザクションのことですが、まずはトランザクションについて説明します。</p>
<h4>トランザクション</h4>
<p>データベースにおけるトランザクションとは、複数の処理を「不可分なひとつのまとまり」として扱う単位で、複数のすべての処理をおこなうか、処理をまったく行わないかのどちらかを保証します。一部の処理は実行され残りの処理は実行されないという状況は決して起こりません。PostgreSQLでのトランザクションの実行方法は、トランザクションを構成するSQL文をBEGINとCOMMITで囲って明示的にトランザクションを実行する方法と、自動COMMIT機能を備えたpsqlなどのツールを使って単一のSQL文を暗黙的にトランザクションとして実行する方法の二通りがあります。</p>
<h4>ロングトランザクション</h4>
<p>ロングトランザクションとは、明示的にトランザクションを実行する方法で、BEGINでトランザクションを開始してから長い時間(明示的な定義は無いですが、典型的には数時間以上もしくは1日(数日)以上)トランザクションをCOMMITやROLLBACKでコミットやロールバックせずに継続した状態が長く続いているトランザクションのことを指します。</p>
<h4>ロングトランザクションの存在による弊害</h4>
<p>ロングトランザクションなどのVACUUMを阻害する原因が存在すると、VACUUMを実行しても削除されたタプル領域の回収が出来ません。<br />
理由は、ロングトランザクションが保持する最も古いトランザクションID(oldest xmin)以降に削除されたタプル領域は、ロングトランザクションが参照している可能性があるためです。<br />
そのため、削除されたタプル領域が回収されて再び利用可能となった領域が全く無いため、そのテーブルへのINSERTやUPDATEでタプルを配置するにはテーブルが使用するファイル末尾の新規の領域を使用する必要があります。<br />
そのため、テーブルが使用するファイルサイズが肥大し続けてしまい、結果的にPostgreSQLが使用するディスク使用率が増え続けます。<br />
また、VACUUMを実行しても削除されたタプル領域の回収が出ないため、VACUUMが実行され続けてしまいますので、CPU使用率やDisk I/Oがずっと高い状況となってしまい、性能にも影響します。</p>
<h4>ロングトランザクションの検知と対策</h4>
<p>ロングトランザクションを検知する方法は、pg_stat_activityのstate列がトランザクションを開始したけれども何も処理をしていない「idle in transaction」状態が長く続く接続を見つけることです。<br />
対応は、ロングトランザクション状態のpidを見つけ出し、その接続を SELECT pg_terminate_backend(&lt;接続のpid&gt;); で強制終了することです。例えば1日以上経過した idle in transaction 状態のロングトランザクションがあれば、そのpidを引数にした SELECT pg_terminate_backend(&lt;接続のpid&gt;); で強制終了します。</p>
<p>ロングトランザクションによるVACUUMの阻害はVACUUMが実行されるサーバ上のロングトランザクションだけでなく、ストリーミングレプリケーションのスタンバイ側でhot_standby_feedback = on と設定した場合のスタンバイ側でのロングトランザクションが影響します。まずは、レプリケーションスロットを使わない場合について説明します。</p>
<pre>2026-03-26 21:19:40.572 JST [23346] LOG: automatic vacuum of table "db1.public.vacuum_test": index scans: 0
pages: 0 removed, 14019 remain, 9347 scanned (66.67% of total), 0 eagerly scanned
tuples: 0 removed, 1499989 remain, 500000 are dead but not yet removable
removable cutoff: 1131, which was 1 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 4.694 MB/s, avg write rate: 17.083 MB/s
buffer usage: 17259 hits, 1493 reads, 5434 dirtied
WAL usage: 2 records, 2 full page images, 10076 bytes, 0 buffers full
system usage: CPU: user: 0.14 s, system: 0.02 s, elapsed: 2.48 s
</pre>
<p>→ tuples: に 500000 行が削除されているが整理できない状態(500000 are dead but not yet removable)と出ています。<br />
removable cutoff: 1131 に出ている 1131 が阻害している oldest xmin になります。</p>
<pre>db1=# SELECT * FROM pg_stat_activity;
 datid | datname | pid   | leader_pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |        backend_start          |          xact_start          |          query_start          |         state_change          | wait_event_type |    wait_event     | state  | backend_xid | backend_xmin | query_id |               query                     | backend_type
-------+---------+-------+------------+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+------------------------------+-------------------------------+-------------------------------+-----------------+-------------------+--------+-------------+--------------+----------+-----------------------------------------+--------------
       |         | 22943 |            | 10       | postgres | walreceiver      |             |                 | -1          | 2026-03-26 21:14:46.048588+09 |                              | 2026-03-26 21:14:46.050372+09 | 2026-03-26 21:14:46.050385+09 | Activity        | WalSenderMain     | active |             | 1131         |          | START_REPLICATION 0/60000000 TIMELINE 1 | walsender
</pre>
<p>→ レプリケーションスロットを使用しない場合、プライマリ側のpg_stat_activityでは、walsenderプロセスのbackend_xminが1131になっていることがわかります。この場合、walsenderプロセスを終了しても問題の解決とはならず、スタンバイ側から新規にストリーミングレプリケーション接続が来てbackend_xminが1131の新たなwalsenderプロセスが起動するだけです。</p>
<p>解決するには、スタンバイ側のpg_stat_activityで、backend_xminが1131のidle in transaction状態の接続を見つけ出し、そのpidを引数にpg_terminate_bakendを実行する必要があります。</p>
<pre>(スタンバイ側)
db1=# SELECT * FROM pg_stat_activity;
 datid | datname | pid   | leader_pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |        backend_start          |          xact_start         |          query_start          |         state_change         | wait_event_type |    wait_event     |         state       | backend_xid | backend_xmin | query_id |              query                | backend_type
-------+---------+-------+------------+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+-----------------------------+-------------------------------+------------------------------+-----------------+-------------------+---------------------+-------------+--------------+----------+-----------------------------------+----------------
 17009 | db1     | 23240 |            | 10       | postgres | psql             |             |                 | -1          | 2026-03-26 21:18:55.374729+09 | 2026-03-26 21:18:58.5415+09 | 2026-03-26 21:19:02.709309+09 | 2026-03-26 21:19:02.776459+09| Client          | ClientRead        | idle in transaction |             | 1131         |          | SELECT COUNT(*) FROM vacuum_test; | client backend
：(略)

db1=# SELECT pg_terminate_backend(23240);
pg_terminate_backend
----------------------
t
(1 row)

db1=# SELECT * FROM pg_stat_activity WHERE pid = 23240;
 datid | datname | pid   | leader_pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |        backend_start          |          xact_start         |          query_start          |         state_change         | wait_event_type |    wait_event     |         state       | backend_xid | backend_xmin | query_id |              query                | backend_type
-------+---------+-------+------------+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+-----------------------------+-------------------------------+------------------------------+-----------------+-------------------+---------------------+-------------+--------------+----------+-----------------------------------+----------------
(0 rows)

db1=#
</pre>
<p>→すると、VACUUMを阻害していた要因であるスタンバイ側のロングトランザクションが終了しましたので、再度VACUUMが実行された時には500000行の「削除された行(dead tuples)」の領域が無事回収されました。</p>
<pre>2026-03-26 21:34:44.148 JST [24529] LOG: automatic vacuum of table "db1.public.vacuum_test": index scans: 1
pages: 0 removed, 14019 remain, 9347 scanned (66.67% of total), 0 eagerly scanned
tuples: 500000 removed, 750064 remain, 0 are dead but not yet removable
removable cutoff: 1133, which was 0 XIDs old when operation ended
frozen: 1 pages from table (0.01% of total) had 11 tuples frozen
visibility map: 9347 pages set all-visible, 4673 pages set all-frozen (0 were all-visible)
index scan needed: 4673 pages from table (33.33% of total) had 500000 dead item identifiers removed
index "vacuum_test_pkey": pages: 4116 in total, 0 newly deleted, 0 currently deleted, 0 reusable
avg read rate: 2.484 MB/s, avg write rate: 16.056 MB/s
buffer usage: 25674 hits, 1870 reads, 12085 dirtied
WAL usage: 21431 records, 12085 full page images, 52668632 bytes, 1633 buffers full
system usage: CPU: user: 0.19 s, system: 0.17 s, elapsed: 5.88 s
</pre>
<p>次に、レプリケーションスロットを使った場合に、スタンバイ側のロングトランザクションがどのように影響するかについて説明します。</p>
<pre>2026-03-26 21:58:39.091 JST [26089] LOG: automatic vacuum of table "db1.public.vacuum_test": index scans: 0
pages: 0 removed, 14019 remain, 9347 scanned (66.67% of total), 0 eagerly scanned
tuples: 0 removed, 1253147 remain, 500000 are dead but not yet removable
removable cutoff: 1137, which was 2 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 18751 hits, 0 reads, 0 dirtied
WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.07 s, elapsed: 0.34 s
</pre>
<p>→ tuples に 500000 行が削除されているが整理できない状態(500000 are dead but not yet removable)と出ています。<br />
removable cutoff: 1137 に出ている 1137 が阻害している oldest xmin になります。</p>
<p>今回は、pg_stat_activityを検索しても、1137は出てきません。</p>
<pre> datid | datname | pid   | leader_pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |        backend_start          |          xact_start          |          query_start          |         state_change          | wait_event_type |    wait_event     |         state       | backend_xid | backend_xmin | query_id |                                query                                        | backend_type
-------+---------+-------+------------+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+------------------------------+-------------------------------+-------------------------------+-----------------+-------------------+---------------------+-------------+--------------+----------+-----------------------------------------------------------------------------+----------------
 17009 | db1     | 26121 |            | 10       | postgres | psql             |             |                 | -1          | 2026-03-26 22:00:12.794388+09 | 2026-03-26 22:01:26.60796+09 | 2026-03-26 22:01:26.60796+09  | 2026-03-26 22:01:26.607966+09 |                 |                   | active              |             | 1139         |          | SELECT * FROM pg_stat_activity WHERE backend_xmin != 0 or backend_xid != 0; | client backend
(1 row)
</pre>
<p>レプリケーションスロットを使用している場合は、pg_replication_slotsを検索する必要があります。</p>
<pre>db1=# SELECT * FROM pg_replication_slots;
 slot_name | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase | two_phase_at |        inactive_since         | conflicting | invalidation_reason | failover | synced
-----------+--------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------+--------------+-------------------------------+-------------+---------------------+----------+--------
 slot1     |        | physical  |        |          | f         | t      | 24976      | 1137 |              | 0/8E267DC0  |                     | reserved   |               | f         |              |                               |             |                     | f        | f
(1 row)

db1=#
</pre>
<p>→ pg_replication_slotsのxmin列が1137となっているのが見つかりました。スタンバイ側のロングトランザクションが参照しているbackend_xminがこのxminに反映されていますので、こちらも対応は先ほどと同様に、スタンバイ側のロングトランザクションを終了させることになります。</p>
<pre>db1=# SELECT * FROM pg_stat_activity WHERE (backend_xmin != 0 or backend_xid != 0) AND pid &lt;&gt; pg_backend_pid();
 datid | datname | pid   | leader_pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |        backend_start          |          xact_start           |          query_start          |         state_change          | wait_event_type |    wait_event     |        state        | backend_xid | backend_xmin | query_id |               query               | backend_type 
-------+---------+-------+------------+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+-----------------+-------------------+---------------------+-------------+--------------+----------+-----------------------------------+----------------
 17009 | db1     | 25106 |            | 10       | postgres | psql             |             |                 | -1          | 2026-03-26 21:51:45.786398+09 | 2026-03-26 21:51:52.469941+09 | 2026-03-26 21:51:58.301049+09 | 2026-03-26 21:51:58.357012+09 | Client          | ClientRead        | idle in transaction |             | 1137         |          | SELECT COUNT(*) FROM vacuum_test; | client backend
(1 row)

db1=# SELECT pg_terminate_backend(25106);
pg_terminate_backend
----------------------
t
(1 row)

db1=#
</pre>
<h3>未使用のレプリケーションスロット(原因その2)</h3>
<p>&nbsp;</p>
<p>レプリケーションスロットはストリーミングレプリケーションでスタンバイが使用するために必要なWALファイルが削除されないようキープしておく機能があります。<br />
レプリケーションスロットを使用するスタンバイがアクティブな場合は、スタンバイが必要なWALファイルはスタンバイに転送され続けますので、プライマリ側にWALファイルが溜まることはありませんが、<br />
先ほどのhot_standby_feedback=onの設定のスタンバイ上でロングトランザクションが存在する状態からスタンバイを停止すると、スタンバイからの接続が無いのに、xminはロングトランザクションが参照していた値(下記の例では1139)が残ってしまいます。activeがfとなっていますので使用されていない状況です。<br />
スタンバイを停止した状況が続いたり、スタンバイを使用しなくなった際にレプリケーションスロットの削除を忘れてしまうと、スタンバイが保持するxminによってVACUUMによる「削除された行(dead tuples)」の回収が出来なくなり、かつトランザクションID(XID)の凍結も出来ない状態が続きます。さらに悪いことに、レプリケーションスロットはスタンバイが使用するためのWALを保持し続けますので、スタンバイが使用しなくなったタイミング以降、pg_walディレクトリの下にWALファイルがどんどんたまり続けて最終的にはストレージの空き容量が無くなる弊害もあります。</p>
<pre>db1=# SELECT * FROM pg_replication_slots;
 slot_name | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase | two_phase_at |        inactive_since         | conflicting | invalidation_reason | failover | synced
-----------+--------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------+--------------+-------------------------------+-------------+---------------------+----------+--------
 slot1     |        | physical  |        |          | f         | f      |            | 1139 |              | 0/914DA680  |                     | reserved   |               | f         |              | 2026-03-26 22:08:48.294834+09 |             |                     | f        | f
(1 row)
</pre>
<p>上記の状況(レプリケーションスロットにxmin 1139が存在する状態)で、xid_wraparoundを使ってxidを1000000000進めたところ、先ほどのvacuum_testテーブルのVACUUMログは積極的なVACUUMを示す「aggressive」が付いたメッセージに変わりました。</p>
<pre>2026-03-26 22:21:04.193 JST [26659] LOG: automatic aggressive vacuum to prevent wraparound of table "db1.public.vacuum_test":
index scans: 0
pages: 0 removed, 14019 remain, 14019 scanned (100.00% of total), 0 eagerly scanned
tuples: 0 removed, 1500000 remain, 500000 are dead but not yet removable
removable cutoff: 1139, which was 1000000004 XIDs old when operation ended
new relfrozenxid: 1139, which is 4 XIDs ahead of previous value
frozen: 9346 pages from table (66.67% of total) had 999989 tuples frozen
visibility map: 0 pages set all-visible, 4672 pages set all-frozen (4672 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 3.869 MB/s, avg write rate: 14.969 MB/s
buffer usage: 25679 hits, 2416 reads, 9347 dirtied
WAL usage: 14019 records, 9347 full page images, 77042801 bytes, 1694 buffers full
system usage: CPU: user: 0.16 s, system: 0.06 s, elapsed: 4.87 s
2026-03-26 22:21:04.193 JST [26659] WARNING: cutoff for removing and freezing tuples is far in the past
2026-03-26 22:21:04.193 JST [26659] HINT: Close open transactions soon to avoid wraparound problems.
You might also need to commit or roll back old prepared transactions, or drop stale replication slots.
</pre>
<p>[対策]<br />
未使用のレプリケーションスロットの削除</p>
<p>上記の未使用のレプリケーションスロットであるslot1を削除します。</p>
<pre>db1=# SELECT * FROM pg_drop_replication_slot('slot1');
pg_drop_replication_slot
--------------------------

(1 row)

db1=# SELECT * FROM pg_replication_slots;
 slot_name | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase | two_phase_at |        inactive_since         | conflicting | invalidation_reason | failover | synced
-----------+--------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------+--------------+-------------------------------+-------------+---------------------+----------+--------
(0 rows)

db1=#
</pre>
<p>すると、削除できなかった500000行が削除されて(tuples: 500000 removed)、削除できなかった行は0になりました(0 are dead but not yet removable)</p>
<pre>2026-03-26 22:25:33.378 JST [27413] LOG: automatic aggressive vacuum to prevent wraparound of table "db1.public.vacuum_test":
index scans: 1
pages: 0 removed, 14019 remain, 9347 scanned (66.67% of total), 0 eagerly scanned
tuples: 500000 removed, 833358 remain, 0 are dead but not yet removable
removable cutoff: 1143, which was 1000000000 XIDs old when operation ended
new relfrozenxid: 1143, which is 4 XIDs ahead of previous value
frozen: 4674 pages from table (33.34% of total) had 500000 tuples frozen
visibility map: 9347 pages set all-visible, 9347 pages set all-frozen (0 were all-visible)
index scan needed: 4674 pages from table (33.34% of total) had 499967 dead item identifiers removed
index "vacuum_test_pkey": pages: 4116 in total, 0 newly deleted, 0 currently deleted, 0 reusable
avg read rate: 3.471 MB/s, avg write rate: 18.201 MB/s
buffer usage: 25789 hits, 1755 reads, 9203 dirtied
WAL usage: 26106 records, 12086 full page images, 52913197 bytes, 2741 buffers full
system usage: CPU: user: 0.30 s, system: 0.07 s, elapsed: 3.95 s
</pre>
<h3>放置されたプリペアドトランザクション(原因3)</h3>
<p>プリペアドトランザクション（PREPARE TRANSACTION）は、2フェーズコミットで使用されるトランザクションであり、COMMIT PREPARED または ROLLBACK PREPARED が実行されるまでトランザクションが完了しません。<br />
そのため、長期間放置されたプリペアドトランザクションは、xminを保持し続け、VACUUMによる行回収を阻害する原因となります。</p>
<p>postgresql.confでmax_prepated_transactions=10と設定し再起動します。<br />
その後、下記のSQLを端末1で実行します。</p>
<pre>db1=# BEGIN;
BEGIN
db1=*# PREPARE TRANSACTION 'transaction1';
PREPARE TRANSACTION
db1=#
</pre>
<p>別の端末2で下記の更新を実行します。</p>
<pre>db1=# UPDATE vacuum_test
SET data = md5(random()::text)
WHERE id &lt;= 500000;
UPDATE 500000
db1=#
</pre>
<p>VACUUMのログは下記のようになります。削除された行の500000行が回収できない、removable cutoffは1000001143と出ています。</p>
<pre>2026-03-26 22:36:16.987 JST [27867] LOG: automatic vacuum of table "db1.public.vacuum_test": index scans: 0
pages: 0 removed, 14019 remain, 9347 scanned (66.67% of total), 0 eagerly scanned
tuples: 0 removed, 1277823 remain, 500000 are dead but not yet removable
removable cutoff: 1000001143, which was 3 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 18751 hits, 0 reads, 0 dirtied
WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full
system usage: CPU: user: 0.06 s, system: 0.00 s, elapsed: 0.31 s
</pre>
<p>しかし、pg_stat_activityにもpg_replication_slotsにも1000001143は出てきません。</p>
<pre>db1=# SELECT * FROM pg_stat_activity WHERE (backend_xmin != 0 or backend_xid != 0) AND pid &lt;&gt; pg_backend_pid();
 datid | datname | pid   | leader_pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |        backend_start          |          xact_start           |          query_start          |         state_change          | wait_event_type |    wait_event     |        state        | backend_xid | backend_xmin | query_id |               query               | backend_type 
-------+---------+-------+------------+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+-----------------+-------------------+---------------------+-------------+--------------+----------+-----------------------------------+----------------
(0 rows)

db1=# SELECT * FROM pg_replication_slots;
 slot_name | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase | two_phase_at |        inactive_since         | conflicting | invalidation_reason | failover | synced
-----------+--------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------+--------------+-------------------------------+-------------+---------------------+----------+--------
(0 rows)

db1=#
</pre>
<p>[対策]<br />
pg_prepared_xacts ビューを確認することで、未完了のプリペアドトランザクションを確認できます。transaction列が1000001143となっていますので、これが原因です。このプリペアドトランザクションをロールバックすることで、xminの固定は解消され、VACUUM処理が進みます。</p>
<pre>db1=# SELECT * FROM pg_prepared_xacts;
 transaction |     gid      |           prepared            |  owner   | database
-------------+--------------+-------------------------------+----------+----------
  1000001143 | transaction1 | 2026-03-26 22:33:40.521517+09 | postgres | db1
(1 row)

db1=# ROLLBACK PREPARED 'transaction1';
ROLLBACK PREPARED
db1=# SELECT * FROM pg_prepared_xacts;
 transaction |     gid      |           prepared            |  owner   | database
-------------+--------------+-------------------------------+----------+----------
(0 rows)

db1=#
</pre>
<p>VACUUMのログを確認すると、削除できなかった500000行は回収され、削除できない行の残りは0行となりました。</p>
<pre>2026-03-26 22:42:23.149 JST [28009] LOG: automatic vacuum of table "db1.public.vacuum_test": index scans: 1
pages: 0 removed, 14019 remain, 9347 scanned (66.67% of total), 0 eagerly scanned
tuples: 500000 removed, 750102 remain, 0 are dead but not yet removable
removable cutoff: 1000001146, which was 0 XIDs old when operation ended
new relfrozenxid: 1000001144, which is 1 XIDs ahead of previous value
frozen: 2 pages from table (0.01% of total) had 74 tuples frozen
visibility map: 9347 pages set all-visible, 4674 pages set all-frozen (0 were all-visible)
index scan needed: 4673 pages from table (33.33% of total) had 499967 dead item identifiers removed
index "vacuum_test_pkey": pages: 4116 in total, 0 newly deleted, 0 currently deleted, 0 reusable
avg read rate: 1.675 MB/s, avg write rate: 14.766 MB/s
buffer usage: 26172 hits, 1371 reads, 12085 dirtied
WAL usage: 21432 records, 12085 full page images, 52668618 bytes, 4692 buffers full
</pre>
<h2>まとめ</h2>
<p>最初に「この記事のポイント」でお伝えした内容を繰り返します。</p>
<p>VACUUMが期待通りに動作しない場合、以下を確認することで原因特定が可能です。</p>
<ul>
<li>ログに「are dead but not yet removable」が出ているか</li>
<li>ログの「removable cutoff (PostgreSQL 16以上) もしくは oldest xmin (PostgreSQL 15以下)」の数字が進んでいないか</li>
<li>以下のビューを確認する(ストリーミングレプリケーション環境の場合はスタンバイ側も)
<ul>
<li>pg_stat_activity</li>
<li>pg_replication_slots</li>
<li>pg_prepared_xacts</li>
</ul>
</li>
</ul>
<p>主な原因は以下の3つです。</p>
<ol>
<li>ロングトランザクション</li>
<li>レプリケーションスロット</li>
<li>プリペアドトランザクション</li>
</ol>
<p>対策の本質は 「xminを固定している要因を解消すること」 です。</p>
<h2>(おまけ)検証方法</h2>
<p>端末1でdb1に接続して、下記のSQL文を実行します。</p>
<pre>db1=# CREATE TABLE vacuum_test (
id bigserial PRIMARY KEY,
data text
);

INSERT INTO vacuum_test(data)
SELECT md5(i::text)
FROM generate_series(1, 1000000) i;

VACUUM (ANALYZE,VERBOSE) vacuum_test;
</pre>
<p>実行ログは下記の通りです。</p>
<pre>CREATE TABLE
INSERT 0 1000000
INFO: vacuuming "db1.public.vacuum_test"
INFO: finished vacuuming "db1.public.vacuum_test": index scans: 0
pages: 0 removed, 9346 remain, 9346 scanned (100.00% of total), 0 eagerly scanned
tuples: 0 removed, 1000000 remain, 0 are dead but not yet removable
removable cutoff: 1134, which was 2 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.344 MB/s
buffer usage: 18714 hits, 0 reads, 2 dirtied
WAL usage: 3 records, 2 full page images, 16740 bytes, 0 buffers full
system usage: CPU: user: 0.03 s, system: 0.00 s, elapsed: 0.04 s
INFO: vacuuming "db1.pg_toast.pg_toast_17023"
INFO: finished vacuuming "db1.pg_toast.pg_toast_17023": index scans: 0
pages: 0 removed, 0 remain, 0 scanned (100.00% of total), 0 eagerly scanned
tuples: 0 removed, 0 remain, 0 are dead but not yet removable
removable cutoff: 1134, which was 2 XIDs old when operation ended
frozen: 0 pages from table (100.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (100.00% of total) had 0 dead item identifiers removed
avg read rate: 12.245 MB/s, avg write rate: 0.000 MB/s
buffer usage: 25 hits, 1 reads, 0 dirtied
WAL usage: 1 records, 0 full page images, 258 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
INFO: analyzing "public.vacuum_test"
INFO: "vacuum_test": scanned 9346 of 9346 pages, containing 1000000 live rows and 0 dead rows; 30000 rows in sample, 1000000 estimated total rows
INFO: finished analyzing table "db1.public.vacuum_test"
avg read rate: 0.638 MB/s, avg write rate: 0.957 MB/s
buffer usage: 9478 hits, 4 reads, 6 dirtied
WAL usage: 10 records, 5 full page images, 23354 bytes, 0 buffers full
system usage: CPU: user: 0.04 s, system: 0.00 s, elapsed: 0.04 s
VACUUM
db1=#
</pre>
<p>次に、別の端末2を立ち上げてdb1に接続し、REPEATABLE READ レベルでトランザクションを開始、先ほど作成したvacuum_testテーブルを参照して放置します。idle in transaction状態となります。</p>
<pre>db1=# BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
db1=* # SELECT COUNT(*) FROM vacuum_test;
count
---------
1000000
(1 row)

db1=*#
</pre>
<p>元の端末1に戻って、500000件UPDATEします。すると「削除された行(dead tuples)」が500000行発生し、自動VACUUMが実行されます。</p>
<pre>db1=# UPDATE vacuum_test
SET data = md5(random()::text)
WHERE id &lt;= 500000;
UPDATE 500000
db1=#
</pre>
<p>しかし、端末2のidle状態のトランザクションにより、「削除された行(dead tuples)」は回収されずに残ったままとなります。この端末2のidle状態のトランザクション(idle in transaction)が長い間続くとロングトランザクションと呼ばれます。</p>
<div>
<a href="https://www.sraoss.co.jp/prod_serv/support/pgsql-mainte/?utm_source=sraoss-techblog&utm_medium=referral&utm_campaign=under-article-pgsql" rel="noopener" style="color:initial;" target="_blank" class="no-ext"></p>
<div style="background-color:#DAEFF6; padding:20px; border-radius:8px; text-align:center;">
SRA OSSでは、PostgreSQL/Pgpool-II のサポートを提供しております。<br />
詳しくは<strong style="color:#2581c4;">弊社ホームページ</strong>をご覧ください。
</div>
<p></a>
</div>

<div>
<div style="background-color: #fffacd; padding: 20px; border-radius: 8px; text-align: center;">2026/10/27（火）「PostgreSQL 19 リリース記念セミナー」開催！<br />
コントリビューターが新機能をわかりやすく解説します。<br />
詳しくは<a href="https://www.sraoss.co.jp/event_seminar/detail/20261027-postgresql19-webinar/?utm_source=techblog&amp;utm_medium=email&amp;utm_campaign=pg19-seminar" target="_blank" rel="noopener"><strong style="color: #2581c4;">こちら</strong></a>をご覧ください。</div>
</div>

]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>pg_repack (オンラインテーブル再編成ツール)</title>
		<link>https://www.sraoss.co.jp/tech-blog/pgsql/pg_repack/</link>
		
		<dc:creator><![CDATA[PostgreSQL技術グループ]]></dc:creator>
		<pubDate>Mon, 30 May 2016 01:00:12 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[pg_reorg]]></category>
		<category><![CDATA[pg_repack]]></category>
		<category><![CDATA[VACUUM]]></category>
		<category><![CDATA[サードパーティーソフトウェア]]></category>
		<guid isPermaLink="false">https://www.sraoss.co.jp/tech-blog/?p=848</guid>

					<description><![CDATA[pg_repack は PostgreSQL のテーブルをオンラインで再編成できるツールです。本記事では pg_repackについて紹介します。 良く知られている通り、PostgreSQL は追記型アーキテクチャを採用し ...]]></description>
										<content:encoded><![CDATA[<p>pg_repack は PostgreSQL のテーブルをオンラインで再編成できるツールです。本記事では pg_repackについて紹介します。</p>
<p>良く知られている通り、PostgreSQL は追記型アーキテクチャを採用しています。UPDATE や DELETE をしても旧データを格納した行はしばらく物理ファイル上に残り、これを VACUUM コマンドや自動 VACUUM で整理して、その領域を再利用可能にする仕組みとなっています。何らかの理由でこれらの手動・自動の VACUUM 処理が実行されなかった場合には、データ格納に使われない不要領域が増加し、性能劣化の原因となります。そのような場合には、CLUSTER コマンドや VACUUM FULL コマンドを使って、テーブルの再編成をするのですが、これらのコマンドは強いロックを取得するため、サービス中の適用が難しいという課題がありました。</p>
<p>pg_repack は CLUSTER または VACUUM FULL と同様に不要領域の削除や行の並び替えることができますが、処理中に強いロックをごく短時間しか取得しないため、サービス中にも実施が可能となります。</p>
<p><span id="more-848"></span></p>
<h2>機能と仕組み</h2>
<p>pg_repack は、参照や更新処理を長時間のブロックをせずに CLUSTER コマンドや VACUUM FULL コマンド相当の処理を実行することができます。また、合わせてインデックスの再作成も行います。</p>
<p>pg_repack は 図1 のように、操作対象テーブルの一時的な作業テーブルを作成します。操作対象テーブルへトリガを設定し、更新ログ表へ再編成中の更新処理を記録しておき、再編成完了後、その差分情報を作業テーブルに反映し、対象テーブルと切り替えます。再編成が作業テーブルで行われるため、オンラインでの再編成が可能となっています。</p>
<p><a href="/tech-blog/wp-content/uploads/2018/11/pg_reorg_flow.png" target="_blank" rel="noopener noreferrer"><img decoding="async" class=" wp-image-852" src="https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/11/pg_reorg_flow.png" alt="オンライン再編成の仕組み" width="90%" srcset="https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/11/pg_reorg_flow.png 720w, https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/11/pg_reorg_flow-300x142.png 300w, https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/11/pg_reorg_flow-304x144.png 304w, https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/11/pg_reorg_flow-282x133.png 282w" sizes="(max-width: 720px) 100vw, 720px" /></a></p>
<h2>動作環境</h2>
<p>pg_repack は以下の環境に対応しています。</p>
<ul>
<li>PostgreSQL バージョン：9.1 から 16 (pg_repack 1.5.0 時点)</li>
<li>OSバージョン：基本的には x86_64 アーキテクチャの Linux 環境なら動作するはずです。本記事の検証環境には、CentOS 7.4 (x86_64) を使いました。Windows 向けには Microsoft Visual C++ を利用してビルドできます。ソースコードの msvc フォルダ内にプロジェクトファイルが用意されています。</li>
</ul>
<p>Windowsバイナリの公式配布はありませんが、<a href="https://www.sraoss.co.jp/prod_serv/support/pgsql-mainte/?utm_source=sraoss-techblog&amp;utm_medium=referral&amp;utm_campaign=under-article-pgsql">SRA OSS の PostgreSQLサポートサービス</a>では pg_repack の Windowsバイナリ提供を行っています。</p>
<h2>インストール</h2>
<p>pg_repack のソースコードは <a href="https://github.com/reorg/pg_repack" target="_blank" rel="noopener noreferrer">プロジェクトページ</a> からダウンロードします。ダウンロード後、PostgreSQL のコマンドにパスが通ったユーザにて、ビルド、インストールします。</p>
<pre>$ cd pg_repack
$ make
$ sudo make install
</pre>
<p>なお、RHEL系のLinuxを使用していて PostgreSQL本体を<a href="https://yum.postgresql.org/repopackages.php">PGDGリポジトリ</a>のパッケージからインストールしている場合には、pg_repack もパッケージからインストールするのが簡単です。</p>
<p>インストールを行った後、pg_repack エクステンションを対象のデータベースに登録します。 下記例ではデータベース testdb を作成して、そこに拡張モジュールを登録しています。</p>
<pre>$ createdb testdb
$ psql testdb -U postgres

testdb=# CREATE EXTENSION pg_repack;
CREATE EXTENSION
testdb=# \dx
                                  List of installed extensions
   Name    | Version |   Schema   |                         Description
-----------+---------+------------+--------------------------------------------------------------
 pg_repack | 1.4.3   | public     | Reorganize tables in PostgreSQL databases with minimal locks
(1 rows)
</pre>
<h2>基本的な使い方</h2>
<p>pg_repack の基本的な使い方を見ていきます。コマンドは PostgreSQL のスーパーユーザと同名でサーバプロセス実行ユーザでもある OS 上の postgres ユーザで行うものとします。</p>
<p>testdb というデータベースの t1 テーブルをテーブル再編成するにはコマンドラインから以下のように実行します。-n は &#8211;no-order の省略形でデータ並び替えをしないという意味で、このコマンドは「VACUUM FULL t1」に相当します。</p>
<pre>$ pg_repack -n -t t1 -d testdb
INFO: ---- reorganize one table with 7 steps. ----
INFO: target table name    : t1
INFO: ---- STEP1. setup ----
INFO: This needs EXCLUSIVE LOCK against the target table.
INFO: ---- STEP2. copy tuples into temp table----
INFO: ---- STEP3. create indexes ----
INFO: ---- STEP4. apply logs  ----
INFO: ---- STEP5. swap tables ----
INFO: This needs EXCLUSIVE LOCK against the target table.
INFO: ---- STEP6. drop old table----
INFO: ---- STEP7. analyze ----
</pre>
<p>-n を付けないとデータ並び替えも行います。以下コマンドは「CLUSTER t1 USING col_id」に相当します。 -o は &#8211;order-by の省略形で、並び替えの基準となるカラムを指定しています。-o を省略すると以前に CLUSTER コマンドを実行したときに指定されたカラム（cluster key と呼ばれます）で並び替えが行われます。</p>
<pre> $ pg_repack -o col_id -t t1 -d testdb
（以下、コマンド出力を省略します）
</pre>
<p>また、データベース名だけを指定すると、そのデータベース内で pg_repack 実行可能なテーブル全てについて pg_repack を実行します。この場合も -n オプションの有無を選べます。-n 無し、すなわちデータ並び替えをする場合には、cluster key のカラムが定まってるテーブルだけが処理対象となります。</p>
<pre>$ pg_repack -n testdb
</pre>
<p>コマンドオプションの詳細は <a href="https://github.com/reorg/pg_repack/blob/master/doc/pg_repack_jp.rst" target="_blank" rel="noopener noreferrer">pg_repack の付属ドキュメント</a> に記載されています。</p>
<h2>動作確認</h2>
<p>実際に動作と効果を確認してみます。本記事の検証環境は CentOS 7.4、PostgreSQL 9.6.10、pg_repack は 1.4 系列 2018 年 10 月時点のコードです。</p>
<p>テーブル t1 を作成し、10,000 件のレコードを初期登録します。この時点でサイズは 632kB です。</p>
<pre>testdb=# CREATE TABLE t1 (id int primary key, val text);
CREATE TABLE
testdb=#
testdb=# SELECT pg_size_pretty(pg_total_relation_size('t1'));
 pg_size_pretty
----------------
 16 kB
(1 row)

testdb=# INSERT INTO t1 VALUES(generate_series(1,10000),'AAA');
INSERT 0 10000
testdb=# SELECT pg_size_pretty(pg_total_relation_size('t1'));
 pg_size_pretty
----------------
 632 kB
(1 row)
</pre>
<p>次に不要領域を作るため、テーブルの全行を UPDATE することを数回繰り返します。</p>
<pre>testdb=# UPDATE t1 SET VAL = 'BBB';
UPDATE 10000
(何度か繰り返す)

testdb=# SELECT pg_size_pretty(pg_total_relation_size('t1'));
 pg_size_pretty
----------------
 4568 kB
(1 row)
</pre>
<p>更新を繰り返し、レコード数が変わらず、テーブルサイズが 632kB から 4568kB まで膨らみました。VACUUM をしても、テーブルの物理サイズは小さくなりません。</p>
<pre>testdb=# VACUUM t1;
VACUUM
testdb=# SELECT pg_size_pretty(pg_total_relation_size('t1'));
 pg_size_pretty
----------------
 4568 kB
(1 row)
</pre>
<p>pg_repack によるテーブル再編成を行います。</p>
<pre>$ pg_repack -n -t t1 -d testdb
</pre>
<p>再編成後、テーブルの物理サイズが小さくなっていることが確認できます。</p>
<pre>$ psql testdb -U postgres

testdb=# SELECT pg_size_pretty(pg_total_relation_size('t1'));
 pg_size_pretty
----------------
 632 kB
(1 row)
</pre>
<p>他にも、以下のように実行することも可能です</p>
<ul>
<li style="list-style-type: none;">
<ul>
<li>-t オプションを複数同時に使用することで、複数のテーブルを指定することができます。</li>
</ul>
</li>
</ul>
<pre>    $ pg_repack -n -t t1 -t t2 -d testdb
</pre>
<ul>
<li style="list-style-type: none;">
<ul>
<li>データベースtestdb全体を4並列実行にてテーブル再編成をかけます。</li>
</ul>
</li>
</ul>
<pre>    $ pg_repack -n -j 4 testdb
</pre>
<ul>
<li style="list-style-type: none;">
<ul>
<li>データベースtestdbテーブル t1 のインデックスのみを再編成します。</li>
</ul>
</li>
</ul>
<pre>    $ pg_repack -d testdb -t t1 --only-indexes
</pre>
<ul>
<li style="list-style-type: none;">
<ul>
<li>テーブル再編成と共にテーブルとインデックスをテーブルスペース tbs1 に移動します。</li>
</ul>
</li>
</ul>
<pre>    $ pg_repack -d testdb --tablespace tbs1 --moveidx
</pre>
<h2>注意事項</h2>
<p>pg_repack を利用する際には以下の点に注意が必要です。</p>
<ul>
<li>ディスク容量一時的な作業テーブルを作るので、対象テーブルとインデックスの 2 倍以上のディスク空き領域が必要です。PostgreSQL 9.0 以降の VACUUM FULL では、対象テーブル・インデックスと同サイズ程度の空き容量を要しましたが、それよりも更にもう一つ分が必要となることに注意してください。</li>
<li>pg_repack の実行中には、VACUUM と ANALYZE 以外の DDL を実行してはいけません。特に TRUNCATEやCREATE INDEX、ALTER TABLE を実行するとデータの整合性が崩れたり、消えてしまうことがあります。</li>
<li>実行対象テーブル対象のテーブルには、たとえ並べ替えをしない場合でも、主キー、または NOT NULL 属性とユニークインデックスを持つカラムが少なくとも一つ必須です。また、一時テーブル、または GiST インデックスがクラスタインデックスとなっているテーブルは操作の対象外です。</li>
<li>pg_repack は、データベース上のスーパーユーザ（通例 postgres ユーザ）のみが実行可能です。</li>
<li>短いタイムアウトでリトライを繰り返すようにして、排他ロックを短時間だけ取得することで、ロック待ちを回避しています。 -T （&#8211;wait-timeout）で指定するタイムアウト時間（デフォルト60秒）を超えてロック取得ができない場合には他プロセスを終了させる動作が生じます。ロック取得できなかった場合に pg_repack の方を中断させるには -D (&#8211;no-kill-backend) を指定してください。</li>
<li>pg_repack の処理過程には、ある時点で実行中のトランザクションが全て終了するのを待つ、というステップがあります。そのため、ロングトランザクション実行が並行していると、それが別のテーブルを対象にした処理であったとしても、pg_repack が長く待ってしまう可能性があります。</li>
</ul>
<h2>pg_reorg について</h2>
<p>さて、今回紹介した pg_repack ですが、これは pg_reorg というソフトウェアの開発プロジェクトから分岐したものです。現在、pg_reorg の開発は停止しており、実質 pg_repack が後継のソフトウェアとなります。</p>
<p>pg_reorg で提供されていた機能は pg_repackで網羅していることに加え、pg_repack でのみ提供している新機能もあるため、現状 pg_reorg を選択する利点はほとんどありません。しかし PostgreSQL9.0系にこれらを導入する場合は、pg_repack は動作環境としてこれを含んでいないため、pg_reorg を選択することになります。</p>
<p>pg_reorg は、バイナリパッケージは古いバージョンのみであるため、ソースコードからインストールするのが良いでしょう。pg_reorg のソースコードは <a href="https://github.com/ossc-db/pg_reorg" target="_blank" rel="noopener noreferrer">プロジェクトページ</a> からダウンロードします。ビルド手順は pg_repack と同様です。</p>
<h2>おわりに</h2>
<p>本稿では PostgreSQL のテーブルをオンラインで編集できるツール pg_repack、pg_reorg の紹介をしました。</p>
<p>何らかの事情で VACUUM が適切に行われない事態となっていたのを長期に見逃して、既にテーブル物理ファイルの肥大化が発生してしまったけれども、サービス停止できないので、VACUUM FULL コマンドは実行できないという場合の対処手段として pg_repack、pg_reorg は極めて有用といえます。</p>
<div>
<a href="https://www.sraoss.co.jp/prod_serv/support/pgsql-mainte/?utm_source=sraoss-techblog&utm_medium=referral&utm_campaign=under-article-pgsql" rel="noopener" style="color:initial;" target="_blank" class="no-ext"></p>
<div style="background-color:#DAEFF6; padding:20px; border-radius:8px; text-align:center;">
SRA OSSでは、PostgreSQL/Pgpool-II のサポートを提供しております。<br />
詳しくは<strong style="color:#2581c4;">弊社ホームページ</strong>をご覧ください。
</div>
<p></a>
</div>

]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
