From d634622a5583bcded073b251d0d59065b3882579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Yhuel?= Date: Wed, 3 Jun 2026 11:09:01 +0200 Subject: [PATCH 1/5] check_last_maintenance : filter out small tables What is the purpose of the `last_vacuum` and `last_analyze` probes? * To be alerted when statistics are reset following a crash, a failover, etc. * To detect large tables that are no longer being processed frequently enough due to the default autovacuum settings, which are not always well-suited for large tables. Alerts for small tables therefore constitute unnecessary noise. --- README | 10 ++++++++++ check_pgactivity | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/README b/README index e6bfc1b..5b3065d 100644 --- a/README +++ b/README @@ -772,6 +772,11 @@ COMPATIBILITY You can use multiple "--exclude REGEX" parameters. + This service supports a "--analyze_table_min_size" parameter to + exclude small relations. The value must be a positive integer + representing the threshold in kilobytes below which the relation + is excluded. This defaults to 1024 (1MB). + Required privileges: unprivileged role able to log in all databases. last_vacuum (8.2+) @@ -810,6 +815,11 @@ COMPATIBILITY You can use multiple "--exclude REGEX" parameters. + This service supports a "--vacuum_table_min_size" parameter to + exclude small relations. The value must be a positive integer + representing the threshold in kilobytes below which the relation + is excluded. This defaults to 10240 (10MB). + Required privileges: unprivileged role able to log in all databases. locks (all) diff --git a/check_pgactivity b/check_pgactivity index 7fb928f..c71ec6a 100755 --- a/check_pgactivity +++ b/check_pgactivity @@ -503,6 +503,8 @@ my %args = ( 'wal_buffers' => undef, 'checkpoint_segments' => undef, 'effective_cache_size' => undef, + 'vacuum_table_min_size' => 10 * 1024, + 'analyze_table_min_size' => 1024, 'no_check_autovacuum' => 0, 'no_check_fsync' => 0, 'no_check_enable' => 0, @@ -5270,6 +5272,7 @@ sub check_last_maintenance { my @rs; my $c_limit; my $w_limit; + my $table_min_size; my @perfdata; my @msg_crit; my @msg_warn; @@ -5285,6 +5288,8 @@ sub check_last_maintenance { my @dbexclude = @{ $args{'dbexclude'} }; my $me = 'POSTGRES_LAST_' . uc($type); + $table_min_size = $args{$type . "_table_min_size"} * 1024; + # warning and critical are mandatory. pod2usage( -message => "FATAL: you must specify critical and warning thresholds.", @@ -5334,6 +5339,7 @@ sub check_last_maintenance { ||'$c_limit $w_limit')) FROM pg_stat_all_tables a WHERE schemaname NOT LIKE 'pg_temp_%' + AND pg_relation_size(relid) > $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) @@ -5364,6 +5370,7 @@ sub check_last_maintenance { FROM pg_stat_all_tables a WHERE schemaname NOT LIKE 'pg_temp_%' AND schemaname NOT LIKE 'pg_toast_temp_%' + AND pg_relation_size(relid) > $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) @@ -5396,6 +5403,7 @@ sub check_last_maintenance { FROM pg_stat_all_tables a WHERE schemaname NOT LIKE 'pg_temp_%' AND schemaname NOT LIKE 'pg_toast_temp_%' + AND pg_relation_size(relid) > $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) @@ -5438,6 +5446,7 @@ sub check_last_maintenance { JOIN pg_class b on a.relid = b.oid WHERE schemaname NOT LIKE 'pg_temp_%' AND schemaname NOT LIKE 'pg_toast_temp_%' + AND pg_relation_size(relid) > $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) @@ -10035,6 +10044,8 @@ GetOptions( 'dump-status-file!', 'dump-bin-file:s', 'effective_cache_size=i', + 'vacuum_table_min_size=i', + 'analyze_table_min_size=i', 'exclude=s', 'format|F=s', 'global-pattern=s', From 981678e8a7529c0b836970183e79cc2d2510eb05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Yhuel?= Date: Wed, 3 Jun 2026 16:21:13 +0200 Subject: [PATCH 2/5] check_last_maintenance: fix tests broken by previous commit --- check_pgactivity | 8 ++++---- t/01-last_analyze.t | 4 ++++ t/01-last_vacuum.t | 50 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/check_pgactivity b/check_pgactivity index c71ec6a..027bf32 100755 --- a/check_pgactivity +++ b/check_pgactivity @@ -5339,7 +5339,7 @@ sub check_last_maintenance { ||'$c_limit $w_limit')) FROM pg_stat_all_tables a WHERE schemaname NOT LIKE 'pg_temp_%' - AND pg_relation_size(relid) > $table_min_size + AND pg_relation_size(relid) >= $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) @@ -5370,7 +5370,7 @@ sub check_last_maintenance { FROM pg_stat_all_tables a WHERE schemaname NOT LIKE 'pg_temp_%' AND schemaname NOT LIKE 'pg_toast_temp_%' - AND pg_relation_size(relid) > $table_min_size + AND pg_relation_size(relid) >= $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) @@ -5403,7 +5403,7 @@ sub check_last_maintenance { FROM pg_stat_all_tables a WHERE schemaname NOT LIKE 'pg_temp_%' AND schemaname NOT LIKE 'pg_toast_temp_%' - AND pg_relation_size(relid) > $table_min_size + AND pg_relation_size(relid) >= $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) @@ -5446,7 +5446,7 @@ sub check_last_maintenance { JOIN pg_class b on a.relid = b.oid WHERE schemaname NOT LIKE 'pg_temp_%' AND schemaname NOT LIKE 'pg_toast_temp_%' - AND pg_relation_size(relid) > $table_min_size + AND pg_relation_size(relid) >= $table_min_size AND schemaname NOT LIKE 'information_schema' AND (('${type}' = 'analyze' AND schemaname NOT LIKE 'pg_toast') -- TOAST tables aren't ANALYZEd OR ('${type}' = 'vacuum')) diff --git a/t/01-last_analyze.t b/t/01-last_analyze.t index c2dea02..39f9ac1 100644 --- a/t/01-last_analyze.t +++ b/t/01-last_analyze.t @@ -50,6 +50,7 @@ sleep(1); $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', + '--analyze_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -106,6 +107,7 @@ SKIP: { $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', + '--analyze_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -145,6 +147,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', + '--analyze_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -182,6 +185,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', + '--analyze_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', diff --git a/t/01-last_vacuum.t b/t/01-last_vacuum.t index b7df764..a69d873 100644 --- a/t/01-last_vacuum.t +++ b/t/01-last_vacuum.t @@ -10,7 +10,7 @@ use warnings; use lib 't/lib'; use pgNode; use TestLib (); -use Test::More tests => 33; +use Test::More tests => 39; my $node = pgNode->get_new_node('prod'); my $pga_data = "$TestLib::tmp_check/pga.data"; @@ -50,6 +50,7 @@ sleep(1); $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', + '--vacuum_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'template1', @@ -107,6 +108,7 @@ SKIP: { $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', + '--vacuum_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -146,6 +148,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', + '--vacuum_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -183,6 +186,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', + '--vacuum_table_min_size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -196,6 +200,50 @@ $node->command_checks_all( [ 'test database with two tables, both vacuumed' ); +# test database with three tables, only one never vacuumed, filtering out small tables + +# we must track the stat activity on pg_class to make sure there was some stat +# activity to avoid the check_pga shortcut when no activity. +($stdout) = $node->psql('testdb', q{ + SELECT n_tup_ins + FROM pg_stat_sys_tables + WHERE relname = 'pg_class' +}); + +$node->psql('testdb', 'CREATE TABLE boo (bar INT)'); + +$node->poll_query_until('testdb', qq{ + SELECT n_tup_ins > $stdout + FROM pg_stat_sys_tables + WHERE relname = 'pg_class' +}); + +@stdout = ( + qr/^Service *: POSTGRES_LAST_VACUUM$/m, + qr/^Returns *: 0 \(OK\)$/m, + qr/^Message *: 1 database\(s\) checked$/m, + qr/^Perfdata *: testdb=.*s warn=3600 crit=864000$/m +); + +# we don't check the [auto][vacuum,analyze]_count here, because we are filtering out +# system tables which were accounted for in the previous test. + +$node->command_checks_all( [ + './check_pgactivity', '--service' => 'last_vacuum', + '--vacuum_table_min_size' => 10, + '--username' => $ENV{'USER'} || 'postgres', + '--format' => 'human', + '--dbname' => 'testdb', + '--status-file' => $pga_data, + '--warning' => '1h', + '--critical' => '10d' + ], + 0, + \@stdout, + [ qr/^$/ ], + 'database with two tables, one never vacuumed, filtering out small tables' +); + ### End of tests ### $node->stop( 'immediate' ); From 6bad3f316db10428bb278e56c4ac4cefe29c8d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Yhuel?= Date: Mon, 8 Jun 2026 15:40:43 +0200 Subject: [PATCH 3/5] check_last_maintenance: review of Benoit and Christophe --- README | 14 ++++++++------ check_pgactivity | 10 +++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README b/README index 5b3065d..ac5d90f 100644 --- a/README +++ b/README @@ -773,9 +773,10 @@ COMPATIBILITY You can use multiple "--exclude REGEX" parameters. This service supports a "--analyze_table_min_size" parameter to - exclude small relations. The value must be a positive integer - representing the threshold in kilobytes below which the relation - is excluded. This defaults to 1024 (1MB). + exclude small relations. We use the `pg_relation_size()` function + to filter out those relations that are smaller than this value, + because the tables and their associated toast tables are processed + separately by autovacuum, and by this probe. This defaults to 1MB. Required privileges: unprivileged role able to log in all databases. @@ -816,9 +817,10 @@ COMPATIBILITY You can use multiple "--exclude REGEX" parameters. This service supports a "--vacuum_table_min_size" parameter to - exclude small relations. The value must be a positive integer - representing the threshold in kilobytes below which the relation - is excluded. This defaults to 10240 (10MB). + exclude small relations. We use the `pg_relation_size()` function + to filter out those relations that are smaller than this value, + because the tables and their associated toast tables are processed + separately by autovacuum, and by this probe. This defaults to 2MB. Required privileges: unprivileged role able to log in all databases. diff --git a/check_pgactivity b/check_pgactivity index 027bf32..91deae4 100755 --- a/check_pgactivity +++ b/check_pgactivity @@ -503,8 +503,8 @@ my %args = ( 'wal_buffers' => undef, 'checkpoint_segments' => undef, 'effective_cache_size' => undef, - 'vacuum_table_min_size' => 10 * 1024, - 'analyze_table_min_size' => 1024, + 'vacuum_table_min_size' => '2mb', + 'analyze_table_min_size'=> '1mb', 'no_check_autovacuum' => 0, 'no_check_fsync' => 0, 'no_check_enable' => 0, @@ -5288,7 +5288,7 @@ sub check_last_maintenance { my @dbexclude = @{ $args{'dbexclude'} }; my $me = 'POSTGRES_LAST_' . uc($type); - $table_min_size = $args{$type . "_table_min_size"} * 1024; + $table_min_size = get_size($args{$type . "_table_min_size"}); # warning and critical are mandatory. pod2usage( @@ -10044,8 +10044,8 @@ GetOptions( 'dump-status-file!', 'dump-bin-file:s', 'effective_cache_size=i', - 'vacuum_table_min_size=i', - 'analyze_table_min_size=i', + 'vacuum_table_min_size=s', + 'analyze_table_min_size=s', 'exclude=s', 'format|F=s', 'global-pattern=s', From 0ad5044f22091e043e8062a5d2f2c92caf942897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Yhuel?= Date: Mon, 8 Jun 2026 15:48:44 +0200 Subject: [PATCH 4/5] last_maintenance: fix up last commit --- t/01-last_vacuum.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/01-last_vacuum.t b/t/01-last_vacuum.t index a69d873..fe5efed 100644 --- a/t/01-last_vacuum.t +++ b/t/01-last_vacuum.t @@ -230,7 +230,7 @@ $node->poll_query_until('testdb', qq{ $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', - '--vacuum_table_min_size' => 10, + '--vacuum_table_min_size' => '10kb', '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', From 8d9141b9999d4eb965a43010f2c927ea3299ba16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Yhuel?= Date: Tue, 9 Jun 2026 09:56:32 +0200 Subject: [PATCH 5/5] last_maintenance: gleu's review use dashes instead of underscores in the names of the options --- README | 4 ++-- check_pgactivity | 10 +++++----- t/01-last_analyze.t | 8 ++++---- t/01-last_vacuum.t | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README b/README index ac5d90f..af57119 100644 --- a/README +++ b/README @@ -772,7 +772,7 @@ COMPATIBILITY You can use multiple "--exclude REGEX" parameters. - This service supports a "--analyze_table_min_size" parameter to + This service supports a "--analyze-table-min-size" parameter to exclude small relations. We use the `pg_relation_size()` function to filter out those relations that are smaller than this value, because the tables and their associated toast tables are processed @@ -816,7 +816,7 @@ COMPATIBILITY You can use multiple "--exclude REGEX" parameters. - This service supports a "--vacuum_table_min_size" parameter to + This service supports a "--vacuum-table-min-size" parameter to exclude small relations. We use the `pg_relation_size()` function to filter out those relations that are smaller than this value, because the tables and their associated toast tables are processed diff --git a/check_pgactivity b/check_pgactivity index 91deae4..f5972e6 100755 --- a/check_pgactivity +++ b/check_pgactivity @@ -503,8 +503,8 @@ my %args = ( 'wal_buffers' => undef, 'checkpoint_segments' => undef, 'effective_cache_size' => undef, - 'vacuum_table_min_size' => '2mb', - 'analyze_table_min_size'=> '1mb', + 'vacuum-table-min-size' => '2mb', + 'analyze-table-min-size'=> '1mb', 'no_check_autovacuum' => 0, 'no_check_fsync' => 0, 'no_check_enable' => 0, @@ -5288,7 +5288,7 @@ sub check_last_maintenance { my @dbexclude = @{ $args{'dbexclude'} }; my $me = 'POSTGRES_LAST_' . uc($type); - $table_min_size = get_size($args{$type . "_table_min_size"}); + $table_min_size = get_size($args{$type . "-table-min-size"}); # warning and critical are mandatory. pod2usage( @@ -10044,8 +10044,8 @@ GetOptions( 'dump-status-file!', 'dump-bin-file:s', 'effective_cache_size=i', - 'vacuum_table_min_size=s', - 'analyze_table_min_size=s', + 'vacuum-table-min-size=s', + 'analyze-table-min-size=s', 'exclude=s', 'format|F=s', 'global-pattern=s', diff --git a/t/01-last_analyze.t b/t/01-last_analyze.t index 39f9ac1..c785e9c 100644 --- a/t/01-last_analyze.t +++ b/t/01-last_analyze.t @@ -50,7 +50,7 @@ sleep(1); $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', - '--analyze_table_min_size' => 0, + '--analyze-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -107,7 +107,7 @@ SKIP: { $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', - '--analyze_table_min_size' => 0, + '--analyze-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -147,7 +147,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', - '--analyze_table_min_size' => 0, + '--analyze-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -185,7 +185,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_analyze', - '--analyze_table_min_size' => 0, + '--analyze-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', diff --git a/t/01-last_vacuum.t b/t/01-last_vacuum.t index fe5efed..a117af7 100644 --- a/t/01-last_vacuum.t +++ b/t/01-last_vacuum.t @@ -50,7 +50,7 @@ sleep(1); $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', - '--vacuum_table_min_size' => 0, + '--vacuum-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'template1', @@ -108,7 +108,7 @@ SKIP: { $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', - '--vacuum_table_min_size' => 0, + '--vacuum-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -148,7 +148,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', - '--vacuum_table_min_size' => 0, + '--vacuum-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -186,7 +186,7 @@ push @stdout, ( $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', - '--vacuum_table_min_size' => 0, + '--vacuum-table-min-size' => 0, '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb', @@ -230,7 +230,7 @@ $node->poll_query_until('testdb', qq{ $node->command_checks_all( [ './check_pgactivity', '--service' => 'last_vacuum', - '--vacuum_table_min_size' => '10kb', + '--vacuum-table-min-size' => '10kb', '--username' => $ENV{'USER'} || 'postgres', '--format' => 'human', '--dbname' => 'testdb',