agora inbox for [email protected]  
help / color / mirror / Atom feed
Unportable code in autoprewarm.c
277+ messages / 3 participants
[nested] [flat]

* Unportable code in autoprewarm.c
@ 2018-05-02 19:21  Tom Lane <[email protected]>
  0 siblings, 1 reply; 277+ messages in thread

From: Tom Lane @ 2018-05-02 19:21 UTC (permalink / raw)
  To: [email protected]

I've been going through compiler warnings from the buildfarm, and
I notice jacana is unhappy about this:

	/* First line of the file is a record count. */
	if (fscanf(file, "<<" INT64_FORMAT ">>\n", &num_elements) != 1)

It's entirely correct to complain, because we only guarantee that
INT64_FORMAT works with snprintf, not with the scanf family of
functions.

Is there a reason why this record count needs to be int64 rather than
plain int, and if so what?  This code is not exactly well documented,
but it looks to me like the number of records should be bounded by
NBuffers, which is an int and is unlikely ever to not be an int.
So I'm inclined to just flush autoprewarm.c's use of int64 counters
altogether.

			regards, tom lane




^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* Re: Unportable code in autoprewarm.c
@ 2018-05-03 00:24  Robert Haas <[email protected]>
  parent: Tom Lane <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Robert Haas @ 2018-05-03 00:24 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Wed, May 2, 2018 at 3:21 PM, Tom Lane <[email protected]> wrote:
> Is there a reason why this record count needs to be int64 rather than
> plain int, and if so what?  This code is not exactly well documented,
> but it looks to me like the number of records should be bounded by
> NBuffers, which is an int and is unlikely ever to not be an int.
> So I'm inclined to just flush autoprewarm.c's use of int64 counters
> altogether.

I don't know of a reason not to make that change.

-- 
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company




^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread

* [PATCH v2] Track skipped tables during vacuum and analyze
@ 2026-03-24 04:09  Yugo Nagata <[email protected]>
  0 siblings, 0 replies; 277+ messages in thread

From: Yugo Nagata @ 2026-03-24 04:09 UTC (permalink / raw)

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0


--Multipart=_Thu__26_Mar_2026_19_22_03_+0900_+1+LQ8ruMpR+Xuy_--





^ permalink  raw  reply  [nested|flat] 277+ messages in thread


end of thread, other threads:[~2026-03-24 04:09 UTC | newest]

Thread overview: 277+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-05-02 19:21 Unportable code in autoprewarm.c Tom Lane <[email protected]>
2018-05-03 00:24 ` Robert Haas <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>
2026-03-24 04:09 [PATCH v2] Track skipped tables during vacuum and analyze Yugo Nagata <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox