【PostgreSQL】AUTOVACUUM の発動条件を DELETE しながら確認してみた

【PostgreSQL】AUTOVACUUM の発動条件を DELETE しながら確認してみた

2026.05.19

PostgreSQL の autovacuum 処理では VACUUM と ANALYZE の 2 つの処理が行われます。

PostgreSQLには、省略可能ですが強く推奨される自動バキュームという機能があります。 これはVACUUMとANALYZEコマンドの実行を自動化することを目的としたものです。 有効にすると、自動バキュームは大量のタプルの挿入、更新、削除があったテーブルを検査します。

24.1.6. 自動バキュームデーモン

今回は、この autovacuum がどういうタイミングで走るのか、
実際に テーブルレコードを DELETE しながら確認していきます。

実行環境

検証のための実行環境は EC2(AL2023)にインストールした PostgreSQL を使用しました。
EC2 に PostgreSQL をインストールする手順としては下記をご参照ください。

https://dev.classmethod.jp/articles/installing-postgresql-on-ec2-al2023/

PostgreSQL 17.8 を利用し、検証していきます。

[ec2-user@ip-xx-xx-xx-xx ~]$ sudo -u postgres psql
psql (17.8)
Type "help" for help.

postgres=# select * from version();
                                                   version                                                    
--------------------------------------------------------------------------------------------------------------
 PostgreSQL 17.8 on x86_64-amazon-linux-gnu, compiled by gcc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-5), 64-bit
(1 row)

サンプルデータ準備

検証用にサンプル DB と 10 万行のレコードを INSERT したテーブルを作成しておきます。

-- DB 作成
postgres=# CREATE DATABASE blog_sample_db;
CREATE DATABASE

-- DB 切り替え
postgres=# \c blog_sample_db
You are now connected to database "blog_sample_db" as user "postgres".

-- テーブル作成
blog_sample_db=# CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT,
  email TEXT
);

-- 10 万行を INSERT
INSERT INTO users (name, email)
SELECT
  'user_' || i,
  'user_' || i || '@example.com'
FROM generate_series(1, 100000) AS i;
CREATE TABLE
INSERT 0 100000

-- 10万行あるか行数の確認
blog_sample_db=# SELECT COUNT(*) FROM users;
 count  
--------
 100000
(1 row)

-- どんなデータが入っているか、最初の 5 行を一例として表示
blog_sample_db=# SELECT * FROM users LIMIT 5;
 id |  name  |       email        
----+--------+--------------------
  1 | user_1 | user_1@example.com
  2 | user_2 | user_2@example.com
  3 | user_3 | user_3@example.com
  4 | user_4 | user_4@example.com
  5 | user_5 | user_5@example.com
(5 rows)

検証1: AUTOVACUUM の発動条件を確認する

VACUUM 閾値を求める

autovacuum はバキューム閾値を超えた際に実行されます。
バキューム閾値の計算式は以下です。

バキューム閾値 = バキューム基礎閾値 + バキューム規模係数 * タプル数

ここで、バキューム基礎閾値はautovacuum_vacuum_threshold、バキューム規模係数はautovacuum_vacuum_scale_factor、タプル数はpg_class.reltuplesです。

24.1.6. 自動バキュームデーモン

バキューム閾値を求めるため、現在の autovacuum 関連のパラメータを確認します。

autovacuum パラメータの確認
-- autovacuum が有効化されているか確認
blog_sample_db=# SHOW autovacuum;
 autovacuum 
------------
 on
(1 row)

blog_sample_db=# SHOW autovacuum_vacuum_threshold;
 autovacuum_vacuum_threshold 
-----------------------------
 50
(1 row)

blog_sample_db=# SHOW autovacuum_vacuum_scale_factor;
 autovacuum_vacuum_scale_factor 
--------------------------------
 0.2
(1 row)

autovacuum_vacuum_threshold (integer)
どのテーブルに対してもVACUUMを起動するために必要な、更新もしくは削除されたタプルの最小数を指定します。 デフォルトは50タプルです。

autovacuum_vacuum_scale_factor (floating point)
VACUUMを起動するか否かを決定するときに、autovacuum_vacuum_thresholdに足し算するテーブル容量の割合を指定します。 デフォルトは0.2(テーブルサイズの20%)です。

19.10. 自動Vacuum作業

pg_class.reltuples も求めます。
ここには、作成した users テーブルの中で生きている(有効な)行数が表示されます。現在は 10 万行です。

blog_sample_db=# SELECT
relname,
reltuples
FROM pg_class
WHERE relname = 'users';
relname | reltuples
---------+-----------
users   |    100000
(1 row)

relname name
テーブル、インデックス、ビューなどの名前

reltuples float4
テーブル内の生きている行数。 これはプランナで使用される単なる推測値です。 VACUUM、ANALYZE、CREATE INDEXなどの一部のDDLコマンドで更新されます。

51.11. pg_class

上記より、公式ドキュメントの計算式に当てはめると、現在のバキューム閾値は 20,050行 となります。

バキューム閾値 = バキューム基礎閾値(autovacuum_vacuum_threshold) + バキューム規模係数(autovacuum_vacuum_scale_factor) * タプル数(pg_class.reltuples)

バキューム閾値 = 50 + 0.2 × 100,000 = 20,050行

バキューム閾値を超える DELETE

まずDELETE前の初期状態を確認します。現在は、n_dead_tup(無効行数) が 0 行です。
各パラメータの詳細は pg_stat_all_tables のドキュメントをご参照ください。

blog_sample_db=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';

 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |     100000 |          0 | 2026-05-18 01:03:37.892824+00 | 2026-05-18 01:03:37.969273+00
(1 row)

続いて、users テーブルに対し DELETE を実行します。

先で求めた通り、バキューム閾値が 20,050行 なので、それを超える 21,000行 を DELETE します。
この場合は、閾値を超えているので autovacuum が自動で実行されるはずです。

DELETE 実行
blog_sample_db=# DELETE FROM users WHERE id <= 21000;
DELETE 21000

DELETE 直後は、n_dead_tup の値に変化はなく、last_autovacuum の時刻も変化がありません。

DELETE 直後はまだ autovacuum されていない
blog_sample_db=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';
 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |      79000 |      21000 | 2026-05-18 01:03:37.892824+00 | 2026-05-18 01:03:37.969273+00
(1 row)

数分待ってみて、再度同じクエリで結果を確認してみます。
すると以下の通り n_dead_tup が 0 になり、last_autovacuum, last_autoanalyze の値も更新されました。
バキューム閾値を超える DELETE 処理を行ったことで、autovacuum が走ることが確認できました。

数分後, autovacuum が自動で走った
blog_sample_db=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';

relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |      79000 |          0 | 2026-05-18 02:57:40.414999+00 | 2026-05-18 02:57:40.455559+00
(1 row)

結果まとめです。

live tuple dead tuple last_autovacuum last_autoanalyze
DELETE前 100,000 0 01:03:37 01:03:37
DELETE直後 79,000 21,000 01:03:37(変化なし) 01:03:37(変化なし)
DELETEから数分後 79,000 0 02:57:40(更新!) 02:57:40(更新!)

検証2: ANALYZEだけ発動してVACUUMが発動しないケースを確認する

冒頭で述べた通り、autovacuum は VACUUM と ANALYZE の 2つの処理を伴います。
一方で、ANALYZE だけ発動するケースもあるのでそちらも確認してみます。

検証をわかりやすくするため、サンプルDBおよび users テーブル(10万件レコード格納)を作り直しておきます。

blog_sample_db=# \c postgres
You are now connected to database "postgres" as user "postgres".

postgres=# DROP DATABASE blog_sample_db;
DROP DATABASE

postgres=# CREATE DATABASE blog_sample_db;
CREATE DATABASE

postgres=# \c blog_sample_db
You are now connected to database "blog_sample_db" as user "postgres".

blog_sample_db=# CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT,
  email TEXT
);
CREATE TABLE

blog_sample_db=# INSERT INTO users (name, email)
SELECT
  'user_' || i,
  'user_' || i || '@example.com'
FROM generate_series(1, 100000) AS i;
INSERT 0 100000

ANALYZE 閾値を求める

autovacuumデーモンが自動でANALYZEを実行する際の閾値は以下です。

解析でも似たような条件が使用されます。 以下で定義される閾値が、
解析閾値 = 解析基礎閾値 + 解析規模係数 * タプル数
前回のANALYZEの後に挿入、更新、削除されたタプル数と比較されます。

24.1.6. 自動バキュームデーモン

上記ドキュメントだと日本語で書いてあるのでわかりにくいのですが、
パラメータに直すと以下の式となります。

解析閾値 = autovacuum_analyze_threshold + autovacuum_analyze_scale_factor * pg_class.reltuples

autovacuum_analyze_threshold (integer)
どのテーブルに対してもANALYZEを起動するのに必要な、挿入、更新、もしくは削除されたタプルの最小数を指定します。 デフォルトは50タプルです。

autovacuum_analyze_scale_factor (floating point)
ANALYZEを起動するか否かを決定するときに、autovacuum_analyze_thresholdに足し算するテーブル容量の割合を指定します。 デフォルトは0.1(テーブルサイズの10%)です。

19.10. 自動Vacuum作業

式で利用する 3 つのパラメータの値を確認します。

blog_sample_db=# SHOW autovacuum_analyze_threshold;
 autovacuum_analyze_threshold 
------------------------------
 50
(1 row)

blog_sample_db=# SHOW autovacuum_analyze_scale_factor;
 autovacuum_analyze_scale_factor 
---------------------------------
 0.1
(1 row)

blog_sample_db=# SELECT
  relname,
  reltuples
FROM pg_class
WHERE relname = 'users';
 relname | reltuples 
---------+-----------
 users   |    100000
(1 row)

上記より、現在の解析閾値は 10,050行 となります。

解析閾値 = 50 + 0.1 × 100,000 = 10,050行

ANALYZE 閾値を超える(VACUUM閾値は超えない) DELETE

前項にて解析閾値を求めました。
検証1で求めたバキューム閾値と並べると以下の通りです。

閾値
ANALYZE閾値 10,050行
VACUUM閾値 20,050行

すなわち 10,050 行より大きい かつ 20,050 行以下 の DELETE処理(dead tuple) が発生した場合、ANALYZEだけ発動してVACUUMは発動しないはずです。
検証してみましょう。

まず初期状態を確認しておきます。

blog_sample_db=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';

 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |     100000 |          0 | 2026-05-18 07:36:05.066232+00 | 2026-05-18 07:36:05.141247+00
(1 row)

閾値ギリギリの 10,051 行 を DELETE します。

blog_sample_db=# DELETE FROM users WHERE id <= 10051;
DELETE 10051

状態が変化したかを確認します。

blog_sample_db=# SELECT                              
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';
 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |      89949 |      10051 | 2026-05-18 07:36:05.066232+00 | 2026-05-18 08:05:05.508741+00
(1 row)

上記の通り、結果が変わりました。
まずは、n_dead_tup が 10051 行と増えています。
そして何より last_autovacuum の時刻には変化がありませんが、last_autoanalyze の時刻は更新されていることがわかります。(すなわち、analyze のみ実行されている)

検証結果より、AUTOVACUUM は VACUUM と ANALYZE の 2 つの処理を伴いますが、それらはあくまで別々に動作していることがわかりました。

live dead last_autovacuum last_autoanalyze
DELETE前 100,000 0 07:36:05 07:36:05
DELETE直後 89,949 10,051 07:36:05(変化なし) 07:36:05(変化なし)
数分後 89,949 10,051 07:36:05(変化なし!) 08:05:05(更新!)
コラム: 10,050行ちょうどを DELETE した時、自動で ANALYZE は走る?

検証してみたところ以下の通り ANALYZE は実行されませんでした。
10,050 の閾値ちょうどだと ANALYZE は走らないことがわかりました。
あくまでも閾値を超過した時に実行されるんですね。

-- before (DELETE 前)
blog_sample_db=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';

 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |     100000 |          0 | 2026-05-18 06:21:03.972934+00 | 2026-05-18 06:21:04.050405+00
(1 row)

-- 10,050件 DELETE する
blog_sample_db=# DELETE FROM users WHERE id <= 10050;
DELETE 10050

-- DELETE直後の状態
blog_sample_db=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';
 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |      89950 |      10050 | 2026-05-18 06:21:03.972934+00 | 2026-05-18 06:21:04.050405+00
(1 row)

-- DELETE から5分以上経過した後の状態 
-- -> last_autoanalyze の時刻に変化がない
-- -> すなわち、analyze が実行されていないことがわかる
blog_sample_db=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';

 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |      89950 |      10050 | 2026-05-18 06:21:03.972934+00 | 2026-05-18 06:21:04.050405+00
(1 row)

検証3: VACUUM 閾値を超えていなくても VACUUM されるケース

ここまでで以下を検証しました。

  • 検証1: VACUUM 閾値を超えると AUTOVACUUM(VACUUM + ANALYZE) が走る
  • 検証2: 削除件数によっては、VACUUM が実行されず ANALYZE のみ実行されるケースがある

検証3 では、VACUUM 閾値を超えていなくても VACUUM されるケースを確認します。

まずは以下のように users テーブルを作り直しておきます。

テーブルを再作成
blog_sample_db=# \c postgres
You are now connected to database "postgres" as user "postgres".

postgres=# DROP DATABASE blog_sample_db;
DROP DATABASE

postgres=# CREATE DATABASE blog_sample_db;
CREATE DATABASE

postgres=# \c blog_sample_db
You are now connected to database "blog_sample_db" as user "postgres".

blog_sample_db=# CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT,
  email TEXT
);
CREATE TABLE

blog_sample_db=# INSERT INTO users (name, email)
SELECT
  'user_' || i,
  'user_' || i || '@example.com'
FROM generate_series(1, 100000) AS i;
INSERT 0 100000

blog_sample_db=# select count(*) from users;
 count  
--------
 100000
(1 row)

blog_sample_db=# select * from users limit 5;
 id |  name  |       email        
----+--------+--------------------
  1 | user_1 | user_1@example.com
  2 | user_2 | user_2@example.com
  3 | user_3 | user_3@example.com
  4 | user_4 | user_4@example.com
  5 | user_5 | user_5@example.com
(5 rows)

現在の VACUUM 閾値と ANALYZE 閾値を求めていきます。

blog_sample_db=# SHOW autovacuum_vacuum_threshold;
 autovacuum_vacuum_threshold 
-----------------------------
 50
(1 row)

blog_sample_db=# SHOW autovacuum_vacuum_scale_factor;
 autovacuum_vacuum_scale_factor 
--------------------------------
 0.2
(1 row)

blog_sample_db=# SHOW autovacuum_analyze_threshold;
 autovacuum_analyze_threshold 
------------------------------
 50
(1 row)

blog_sample_db=# SHOW autovacuum_analyze_scale_factor;
 autovacuum_analyze_scale_factor 
---------------------------------
 0.1
(1 row)

blog_sample_db=# SELECT
  relname,
  reltuples
FROM pg_class
WHERE relname = 'users';
 relname | reltuples 
---------+-----------
 users   |    100000
(1 row)

上記よりそれぞれの閾値は以下となります。

閾値
VACUUM閾値  = 50 + 0.2 × 100,000 = 20,050行
ANALYZE閾値 = 50 + 0.1 × 100,000 = 10,050行

ここで、17,000 行の DELETE を行ってみます。
この場合、ANALYZE 閾値(10,050行)は超えていますが、VACUUM 閾値(20,050行)は超えていません。
そのため ANALYZE のみ実行されるはずですが、本当にそうなるか確認してみましょう。

まず現在の状態を確認(reltuples の値も併せて確認)

postgres=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';
 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |     100000 |          0 | 2026-05-19 02:55:59.492927+00 | 2026-05-19 02:55:59.573787+00
(1 row)

postgres=# SELECT
  relname,
  reltuples
FROM pg_class
WHERE relname = 'users';

 relname | reltuples 
---------+-----------
 users   |    100000
(1 row)

実際に DELETE を実行します

blog_sample_db=# DELETE FROM users WHERE id <= 17000;
DELETE 17000

DELETE 直後すぐの状態。VACUUM も ANALYZE も日付が変わっていないので、実行されていません。
reltuples の値も変化がありません。

postgres=# SELECT                              
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';

 relname | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze        
---------+------------+------------+-------------------------------+-------------------------------
 users   |      83000 |      17000 | 2026-05-19 02:55:59.492927+00 | 2026-05-19 02:55:59.573787+00
(1 row)

postgres=# SELECT                              
  relname,
  reltuples
FROM pg_class
WHERE relname = 'users';

 relname | reltuples 
---------+-----------
 users   |    100000
(1 row)

DELETE から数分待って再度状態を確認します。
すると以下の通り、VACUUM と ANALYZE が実行されて時刻の値が変わりました。

postgres=# SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'users';

 relname | n_live_tup | n_dead_tup |       last_autovacuum        |       last_autoanalyze        
---------+------------+------------+------------------------------+-------------------------------
 users   |      68849 |          0 | 2026-05-19 02:59:59.61072+00 | 2026-05-19 02:58:59.607179+00
(1 row)

postgres=# SELECT
  relname,
  reltuples
FROM pg_class
WHERE relname = 'users';

 relname | reltuples 
---------+-----------
 users   |     68849
(1 row)

※ 上記結果で、n_live_tup と reltuples の値が 83000(=100,000-17,000) ではなく 68849 になっていますが、これはあくまで推定値のためです。ここは今回のように正確な値にならないこともあります。(reltuples, n_live_tup のドキュメントを参照)

結果をまとめると以下の通りです。

live dead last_autovacuum last_autoanalyze
DELETE前 100,000 0 02:55:59 02:55:59
DELETE直後 83,000 17,000 02:55:59(変化なし) 02:55:59(変化なし)
数分後 68,849 0 02:59:59(更新!) 02:58:59(更新!)

なぜ VACUUM 閾値(20,050行)を超えていない 17,000行の DELETE なのに、
VACUUM まで実行されたのかですが、これは ANALYZE によって reltuples の値が更新されたためです。
具体的には、以下の流れです。

17,000行の DELETE により ANALYZE閾値(10,050行)を超えた

ANALYZE が走り、統計情報(reltuples)が更新された
   reltuples: 100,000 → 68,849(推定値)

VACUUM閾値が動的に再計算された
   50 + 0.2 × 68,849 ≒ 13,820行

dead tuple(17,000行)が新しいVACUUM閾値(13,820行)を超えた

VACUUM が走った

ANALYZE が先に走って reltuples を更新したことでVACUUM閾値が 20,050行 -> 13,820行 に下がり、結果的に VACUUM も実行されました。

タイムラインを見ると
・02:58:59 -> ANALYZE が先に実行
・02:59:59 -> 1分後に VACUUM が実行

という順番で処理されていたことも確認できます。

今回の検証3で autovacuum では、VACUUM 閾値を超えていなくても VACUUM が行われることがあることがわかりました。

検証は以上です。

終わりに

今回は autovacuum がどのタイミングで走るのかを、実際に DELETE を行いながら確認しました。

3つの検証結果をまとめると以下の通りです。

削除行数 VACUUM ANALYZE
検証1 21,000行(VACUUM閾値超え) ✅ 実行 ✅ 実行
検証2 10,051行(ANALYZE閾値超え・VACUUM閾値未満) ❌ 実行されず ✅ 実行
検証3 17,000行(ANALYZE閾値超え・VACUUM閾値未満) ✅ 実行 ✅ 実行

検証3が特に面白くて、VACUUM 閾値を超えていない削除でも ANALYZE が先に走って reltuples が更新されることで VACUUM 閾値が動的に変わり、結果的に VACUUM も実行されることがわかりました。

autovacuum は「設定した閾値を超えたら走る」というシンプルな仕組みに見えて、
ANALYZE と VACUUM が連携して動作する奥深い仕組みになっていました。

本ブログがどなたかの理解の助けになれば幸いです。

参考文献

https://dev.classmethod.jp/articles/installing-postgresql-on-ec2-al2023/

https://www.postgresql.jp/document/17/html/routine-vacuuming.html#AUTOVACUUM

https://www.postgresql.jp/document/17/html/catalog-pg-class.html#CATALOG-PG-CLASS

https://www.postgresql.jp/document/17/html/monitoring-stats.html#MONITORING-PG-STAT-ALL-TABLES-VIEW
https://www.postgresql.jp/document/17/html/runtime-config-autovacuum.html

この記事をシェアする

関連記事